I have a UITableView with a custom cell. The custom cell as three buttons covering the entire span of the cell. I am trying to scroll the tableview automatically depending on which label the user selects.
I tried using NotificationCenter but it crashes when trying to scroll. Is there a more elegant solution?
Edit: My end goal is to have the user click on optionOne and have it scroll to the cell immediately below. The trick is doing this without cellForRowAt because I'm handling it in the cell due to the multiple buttons.
Custom Cell:
#IBOutlet weak var optionOneLabel: UILabel!
#IBOutlet weak var optionTwoLabel: UILabel!
#IBOutlet weak var optionThreeLabel: UILabel!
override func prepareForReuse() {
super.prepareForReuse()
let optionOne = UITapGestureRecognizer(target: self, action: #selector(CustomCell.optionOnePressed(_:)))
optionOneLabel.addGestureRecognizer(optionOne)
let optionTwo = UITapGestureRecognizer(target: self, action: #selector(CustomCell.optionTwoPressed(_:)))
optionTwoLabel.addGestureRecognizer(optionTwo)
let optionThree = UITapGestureRecognizer(target: self, action: #selector(CustomCell.optionThreePressed(_:)))
optionThreeLabel.addGestureRecognizer(optionThree)
}
#objc func optionOnePressed(_ sender: UITapGestureRecognizer) {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "scrollTableOptionOne"), object: nil)
}
#objc func optionTwoPressed(_ sender: UITapGestureRecognizer) {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "scrollTableOptionTwo"), object: nil)
}
#objc func optionThreePressed(_ sender: UITapGestureRecognizer) {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "scrollTableOptionthree"), object: nil)
}
TableViewController
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(optionOne), name: NSNotification.Name(rawValue: "scrollTableOptionOne"), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(optionTwo), name: NSNotification.Name(rawValue: "scrollTableOptionTwo"), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(optionThree), name: NSNotification.Name(rawValue: "scrollTableOptionThree"), object: nil)
}
#objc func optionOne() {
let indexPath = NSIndexPath(item: posts.count, section: 0)
tableView.scrollToRow(at: indexPath as IndexPath, at: UITableViewScrollPosition.init(rawValue: indexPath.row + 2)!, animated: true)
}
#objc func optionTwo() {
print("Don't scroll this one for now")
}
#objc func optionThree() {
print("Don't scroll this one for now")
}
Let me know if you need more information. Thanks.
Rather than use notifications, you could set up a callback on the cell with the buttons so that when one of them is tapped, the callback is run with the information about which button was tapped so that the view controller can do what it needs to with it:
As an example of what you want to do I've created a sample project which you can download and look at (Xcode 9.4.1, Swift 4.1) It's a bit messy, but it does what you need: https://bitbucket.org/abizern/so51758928/src/master/
The specifics are, firstly, set up the cell with the buttons to handle the callback:
class SelectionCell: UITableViewCell {
enum Button {
case first
case second
case third
}
var callback: ((Button) -> Void)?
#IBAction func firstButtonSelected(_ sender: UIButton) {
callback?(.first)
}
#IBAction func secondButtonSelected(_ sender: UIButton) {
callback?(.second)
}
#IBAction func thirdButtonSelected(_ sender: UIButton) {
callback?(.third)
}
}
Using an enum for the buttons makes the code cleaner than just raw numbers to identify a button. Each button calls the callback identifying itself.
In the table view controller, when you set up the cells in the delegate, you pass this callback to the cell:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let value = values[indexPath.row]
switch value {
case .header:
let cell = tableView.dequeueReusableCell(withIdentifier: "Selection", for: indexPath) as! SelectionCell
cell.callback = handler
return cell
case .value:
let cell = tableView.dequeueReusableCell(withIdentifier: "Display", for: indexPath) as! DisplayCell
cell.configure(with: value)
return cell
}
}
Note, I've passed handler not handler() so that it passed the closure, not the result of running the closure, to the cell. It's the cell that runs the closure.
The callback looks like this (for my example, your needs will be different)
I'm using the button identifier that is passed in to the closure to identify the IndexPath that I'm interested in and scrolling it to the top
private func handler(button: SelectionCell.Button) -> Void {
print("selected \(button)")
let index: Int?
switch button {
case .first:
index = values.index(where: { (value) -> Bool in
if case .value("First") = value {
return true
}
return false
})
case .second:
index = values.index(where: { (value) -> Bool in
if case .value("Second") = value {
return true
}
return false
})
case .third:
index = values.index(where: { (value) -> Bool in
if case .value("Third") = value {
return true
}
return false
})
}
guard let row = index else { return }
let indexPath = IndexPath(row: row, section: 0)
tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
As I've already said, you can look at the example project to see how this all works, but these are the important steps.
Use the cell that sends the notification as the sender object instead of nil. In your cell do:
NotificationCenter.default.post(
name: NSNotification.Name(rawValue: "scrollTableOptionOne"),
object: self)
Change your notification handler so that it can access to the notification. Retrieve the cell from the notification and find out the indexPath.
#objc func optionOne(ntf: Notification) {
let cell = ntf.object as! UITableViewCell
if let indexPath = tableView.indexPath(for: cell) {
tableView.scrollToRow(at: indexPath,
at: .bottom, animated: true)
}
}
I think this is the reason for crashing.
For performance reasons, you should only reset attributes of the cell
that are not related to content, for example, alpha, editing, and
selection state. The table view's delegate in
tableView(_:cellForRowAt:) should always reset all content when
reusing a cell
Refer this
You can make use of Closures or Delegate instead of going with notification
I think you need to change code like this
let indexPath = IndexPath(row: posts.count - 1, section: 0)
tableView.scrollToRow(at: indexPath, at: .top, animated: true)
This will scroll your row to top in table view.
ScrollToRow method having some problem. UITableViewScrollPosition holds the values - none, top, middle, bottom enum values.
Change the line:
tableView.scrollToRow(at: indexPath as IndexPath, at: UITableViewScrollPosition.init(rawValue: indexPath.row + 2)!, animated: true)
To:
tableView.scrollToRow(at: indexPath as IndexPath, at: .top, animated: true)
In order to track indexpath, set row value to cell's tag, in cellForRow:
cell.tag = indexPath.row
And post notification as:
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "scrollTableOptionOne"), object: self.tag)
#objc func optionOne(_ notification: Notification) {
let indexPath = NSIndexPath(item: notification.object+1, section: 0)
tableView.scrollToRow(at: indexPath as IndexPath, at: UITableViewScrollPosition.init(rawValue: indexPath.row + 2)!, animated: true)
}
Better approach will be using closures:
#IBOutlet weak var optionOneLabel: UILabel!
#IBOutlet weak var optionTwoLabel: UILabel!
#IBOutlet weak var optionThreeLabel: UILabel!
var callBackOnLabel : (()->())? /// Add this in cell.
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
let optionOne = UITapGestureRecognizer(target: self, action: #selector(CustomCell.optionOnePressed(_:)))
optionOneLabel.addGestureRecognizer(optionOne)
}
#objc func optionOnePressed(_ sender: UITapGestureRecognizer) {
self.callBackOnLabel?()
}
Then in your cellForRowAt:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
cell.callBackOnLabel = {
let selectedIndex = IndexPath(row: posts.count, section: 0)
tableView.scrollToRow(at: selectedIndex, at: .middle, animated: true)
}
}
In the same way you can add for other two labels or you can make it generic to accept the response in single call back and perform the animation.
Related
I have view controller in which there are multiple section of tableview. In section 0 I have multiple row . Each row having button named as Add Comments when I click on button it pushes me to other view controller having text field when i wrote something and press done button then through delegate I passes textfield data and set it in button title. But problem is my button present in all row changes value. I want only selected row in section changes its button title. below is my code of first viewcontroller
class MyTabViewController: UIViewController {
var addCommentsValueStore: String = "Add Comments"
#IBOutlet weak var tabTableView : ContentWrappingTableView!
#IBAction func addCommentsAction(_ sender: UIButton) {
guard let nextVC = MyCommentsRouter.getMyCommentsViewScreen() else { return }
nextVC.passAddCommentsDelegate = self
self.navigationController?.pushViewController(nextVC, animated: true)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if(indexPath.section == 0)
{
let indetifier = "MyTabTableViewCell"
let cell = tableView.dequeueReusableCell(withIdentifier: indetifier, for: indexPath) as! MyTabTableViewCell
cell.addCommentsButton.setTitle(addCommentsValueStore, for: UIControl.State.normal)
}
return cell
}
extension MyTabViewController: AddCommentsDelegate{
func passAddComments(instruction: String) {
addCommentsValueStore = instruction
print(addCommentsValueStore)
}
}
}
below is code of second view controller:
import UIKit
protocol AddCommentsDelegate{
func passAddComments(instruction: String)
}
class MyCommentsViewController: UIViewController {
#IBOutlet var addCommentsTextField: UITextField!
var passAddCommentsDelegate: AddCommentsDelegate?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func backActionClick(_ sender: UIButton) {
self.navigationController?.popViewController(animated: true)
}
#IBAction func DoneActionClick(_ sender: Any) {
let dataToBeSent = addCommentsTextField.text
self.passAddCommentsDelegate?.passAddComments(instruction: dataToBeSent!)
self.navigationController?.popViewController(animated: true)
}
}
by using tag on the cell button, you can fix your problem.
on your cellForRowAt delegate method, put this line:
cell.addCommentsButton.tag = indexPath.item
now you know exactly which button did select, then you can use this tag to specify which row in your tableView should change its title.
your implementation has some problems:
first of all, the addCommentsValueStore have to be an array of strings.
var addCommentsValueStore: [String] = []
then tell the delegate to show the right title:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: MyTabTableViewCell!
if (indexPath.section == 0) {
cell = tableView.dequeueReusableCell(withIdentifier: "MyTabTableViewCell", for: indexPath) as! MyTabTableViewCell
cell.tag = indexPath.item
cell.addCommentsButton.setTitle(addCommentsValueStore[indexPath.item], for: UIControl.State.normal)
}
return cell
}
the AddCommentsDelegate should return the index too:
protocol AddCommentsDelegate {
func passAddComments(instruction: String, atIndex: Int)
}
then every time you want to pass comment to another viewController, you should pass the index too.
by using this index, you will specify where you should change the row button title.
#UIBotton func DoneActionClick(_ sender: UIButton) {
let dataToBeSent = addCommentsTextField[sender.tag].text
self.passAddCommentsDelegate?.passAddComments(instruction: dataToBeSent!, atIndexPath: sender.tag)
self.navigationController?.popViewController(animated: true)
}
My first posting on stackoverflow, so sorry if its not right
I am using the Master/Detail View template in XCode. This is a BLT Central app and it gets notified of events happening on the BLE device.
I have a Master view, this is updated using
public func UpdateView() {
tableView.beginUpdates()
tableView.reloadRows(at: self.tableView.indexPathsForVisibleRows!, with: .none)
tableView.endUpdates()
}
This works fine and the TableView shows the updates live whenever the BLE notifies
However I also want to update the detail view live, incase this is being shown.
I have the label linked in the DetailView via:
#IBOutlet weak var detailDescription: UILabel!
And it updates just fine when the MasterView seques to the DetailView
However if I try to update the Label when the BLE notify arrives the #IBOutlet detailDescription has turned to nil, so the update fails (Label not linked)
func UpdateDetail() {
guard let label = detailDescription else {
return; //Label not linked
}
label.text = "New Data"
}
The UpdateDetail() function is also used in viewDidLoad() and in that case its working fine
Whats really weird is that if I add a timer in the DetailView to just do the update
var timer : Timer? = nil
#objc func fireTimer() {
DispatchQueue.main.async {
self.UpdateDetail()
}
}
It works fine, so its possibly something to do with calling the UpdateDetail() function from outside the Detail class.
I have checked if the detailDescription reference gets reset by adding a didSet to the property, and its only called once when the view is loaded
Guess I could use the timer work around, but I am totally baffled why the detailItem appears as nil sometimes, so would be grateful for a sanity check.
UPDATE Gone back to basics_______
So I have now gone back to the standard Apple Master->Detail view template and added a simple timer which updates the list. The ListView updates fine, however it still does not update the detail view dynamically. outside its own class.
I am using on a 1 second timer after MainView is loaded, the list view updates fine, however the detail does not.
When debugging it steps into configureView() fine, but detailDescriptionLabel is nil after the first viewDidLoad()
Have tried all the suggestions below, however in each case the weak reference to the label is nil after the initial Load
Totally baffled
#objc func doTimer() {
for index in 0..<objects.count {
objects[index]=(objects[index] as! NSDate).addingTimeInterval(1)
}
tableView.beginUpdates()
tableView.reloadRows(at: self.tableView.indexPathsForVisibleRows!, with: .none)
tableView.endUpdates()
DispatchQueue.main.async {
self.detailViewController?.configureView()
}
}
Full code for MasterViewController.swift here, rest is the same as standard template.
import UIKit
class MasterViewController: UITableViewController {
var detailViewController: DetailViewController? = nil
var objects = [Any]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
navigationItem.leftBarButtonItem = editButtonItem
let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(insertNewObject(_:)))
navigationItem.rightBarButtonItem = addButton
if let split = splitViewController {
let controllers = split.viewControllers
detailViewController = (controllers[controllers.count-1] as! UINavigationController).topViewController as? DetailViewController
}
//Added timer here
_ = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(doTimer), userInfo: nil, repeats: true)
}
//Update data in list
#objc func doTimer() {
for index in 0..<objects.count {
objects[index]=(objects[index] as! NSDate).addingTimeInterval(1)
}
tableView.beginUpdates()
tableView.reloadRows(at: self.tableView.indexPathsForVisibleRows!, with: .none)
tableView.endUpdates()
DispatchQueue.main.async {
self.detailViewController?.configureView()
}
}
override func viewWillAppear(_ animated: Bool) {
clearsSelectionOnViewWillAppear = splitViewController!.isCollapsed
super.viewWillAppear(animated)
}
#objc
func insertNewObject(_ sender: Any) {
objects.insert(NSDate(), at: 0)
let indexPath = IndexPath(row: 0, section: 0)
tableView.insertRows(at: [indexPath], with: .automatic)
}
// MARK: - Segues
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetail" {
if let indexPath = tableView.indexPathForSelectedRow {
let object = objects[indexPath.row] as! NSDate
let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
controller.detailItem = object
controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
controller.navigationItem.leftItemsSupplementBackButton = true
}
}
}
// MARK: - Table View
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let object = objects[indexPath.row] as! NSDate
cell.textLabel!.text = object.description
return cell
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
objects.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
}
}
}
My answer is based on the provided Git repository.
In MasterViewController - you are calling self.detailViewController?.configureView() but you never assign the detail controller and because it's nil it fails to call the configureView function. You can do that in prepare(for segue: UIStoryboardSegue, sender: Any?) by setting self.detailViewController = controller
This won't still help you with the update of the label value.
The reason fo that is because in #objc func doTimer() { you are always setting (overriding) a new Date object into your array (what you probably aimed for is to update value for same reference). Because of this, the reference you assigned to detailViewController is different and you never update the detailItem in your timer. Hence calling the configureView won't make any change as the value remained the same.
It's happening because your detailDescription label is weak "weak var detailDescription". and each time you write this code
guard let label = detailDescription else {
return; //Label not linked
}
label.text = "New Data"
you create a new object and assign value to that object. So the original value for detailDescription label object doesn't change. Try using the same detailDescription object everytime when you change its value.
Image of the tableview
I have a tableview with a collection view in each cell, all linked to an array. Each collection view has tags, so when I have stuff in the array from the beginning, all tableview cells and collection view cells appear properly in the app. But when I add an element to the array in the app itself (I have a second view controller with the stuff to do that), it works but the new table view cell only appears after the screen rotates (really odd). I have tried adding an object of the view controller with the table view in the second view controller where I add an element to the array. Then in the second view controller in ViewWillDisappear, I reloadData() through that object like this:
var vc : ViewController? = ViewController()
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
vc?.listOfActs.reloadData()
}
But this results in an EXC_BAD_INSTRUCTION
Then I tried adding self.listOfActs.reloadData() in the prepareForSegue in the view controller with the table view just so that I could see that it at least refreshes the data at some point in time but even that doesn't work when I click on add scene a second time.
UPDATE: New MainViewController
This is the new first view controller with the table view. I renamed it and have implemented the method for adding to array and reloading. It kind of works if I use an if let on the reloadData but then I'm back to square one where it only updates when I rotate the screen. When I get rid of the if let so it can actually try to update the table view, it gives me a Fata error: unexpectedly found a nil while unwrapping.
class MainViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
//The Table View
#IBOutlet var AddActButton: UIButton!
#IBOutlet weak var listOfActs: UITableView!
var sectionTapped : Int?
var indexitemTapped : Int?
override func viewDidLoad() {
super.viewDidLoad()
listOfActs.delegate = self
listOfActs.dataSource = self
}
//Table View Functions
func numberOfSections(in tableView: UITableView) -> Int {
return actsCollection.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "actCell", for: indexPath) as! ActCell
cell.setCollectionViewDataSourceDelegate(self, forSection: indexPath.section)
return cell
}
//Add To Table View
func addObjects(appendage: Act) {
actsCollection.append(appendage)
if let shit = listOfActs {
shit.reloadData()
}
}
//Header Cell
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cellHeader = tableView.dequeueReusableCell(withIdentifier: "headerCell") as! HeaderCell
cellHeader.headerName.text = actsCollection[section].actName
return cellHeader
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 40
}
}
//Scene Collection in Act Cell
extension MainViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return actsCollection[collectionView.tag].actScenes.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "sceneCell", for: indexPath) as! SceneCell
cell.sceneTitle.text = actsCollection[collectionView.tag].actScenes[indexPath.item].sceneTitle
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
sectionTapped = collectionView.tag
indexitemTapped = indexPath.item
performSegue(withIdentifier: "showDetail", sender: self)
}
//Segue Prepare
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetail" {
let detailsVC = segue.destination as! SceneDetailController
detailsVC.textToAppearInSceneName = actsCollection[sectionTapped!].actScenes[indexitemTapped!].sceneTitle
}
}
}
UPDATE:New second view controller, the one that adds to the array.
class AddActController: UIViewController, UITextFieldDelegate {
#IBOutlet var sceneLiveName: UILabel!
#IBOutlet var sceneNameTextField: UITextField!
#IBOutlet var sceneDescriptionTextField: UITextField!
#IBOutlet var AddSceneButton: UIButton!
#IBOutlet var cardBounds: UIView!
var newName: String? = ""
#IBOutlet var cardShadow: UIView!
var shit = MainViewController()
override func viewDidLoad() {
super.viewDidLoad()
sceneNameTextField.delegate = self
AddSceneButton.alpha = 0.0
cardBounds.layer.cornerRadius = 20.0
cardShadow.layer.shadowRadius = 25.0
cardShadow.layer.shadowOpacity = 0.4
// Do any additional setup after loading the view.
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 0.2){
self.AddSceneButton.alpha = 1.0
}
}
#IBAction func exitButton(_ sender: UIButton) {
dismiss(animated: true, completion: nil)
}
#IBAction func addSceneButton(_ sender: UIButton) {
if newName == "" {
sceneLiveName.text = "Enter Something"
sceneNameTextField.text = ""
}
else {
let appendAct: Act = Act(actName: newName!, actTheme: "Action", actScenes: [Scene(sceneTitle: "Add Act", sceneDescription: "")])
shit.addObjects(appendage: appendAct)
dismiss(animated: true, completion: nil)
}
}
//MARK: textField
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let text: NSString = (sceneNameTextField.text ?? "") as NSString
let resultString = text.replacingCharacters(in: range, with: string)
sceneLiveName.text = resultString
newName = String(describing: (sceneLiveName.text)!.trimmingCharacters(in: .whitespacesAndNewlines))
return true
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
sceneNameTextField.resignFirstResponder()
return true
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
Here is the class for the uitableviewcell that contains its own collection view.
class ActCell: UITableViewCell {
#IBOutlet fileprivate weak var sceneCollection: UICollectionView!
}
extension ActCell {
func setCollectionViewDataSourceDelegate<D: UICollectionViewDataSource & UICollectionViewDelegate>(_ dataSourceDelegate: D, forSection section: Int) {
sceneCollection.delegate = dataSourceDelegate
sceneCollection.dataSource = dataSourceDelegate
sceneCollection.tag = section
sceneCollection.reloadData()
}
}
And here is the model with the user's data including the acts and scenes.
struct Scene {
var sceneTitle: String
var sceneDescription: String
//var characters: [Character]
//var location: Location
}
struct Act {
var actName: String
var actTheme: String
var actScenes : [Scene] = []
}
var actsCollection : [Act] = [
Act(actName: "dfdsfdsfdsf", actTheme: "Action", actScenes: [Scene(sceneTitle: "Example Act", sceneDescription: "")])
]
Any help is greatly appreciated. Thank you to all.
So if I'm not mistaken I believe the viewDidLoad method gets call during screen rotations. So this explains why it update during so. Now to get it to update without rotating the device, I would add an observer in the notificationCenter to watch for any updates to the tableView then call a #selector to do the reloadData(). So here is an example of this. In the viewDidLoad method add
NotificationCenter.default.addObserver(self, selector: #selector(refreshTable), name: NSNotification.Name(rawValue: "load"), object: nil)
Then add the method refreshTable()
func refreshTable() {
listOfActs.reloadData()
}
This is basically how I handle keeping the tableView refreshed.
Well - viewDidLoad is loaded only for the first time controller loads his view (not sure about rotation).
If you really need - you can reload tableView in viewWillAppear - but I wouldn't do this.
Instead of
actsCollection.append(appendAct)
dismiss(animated: true, completion: nil)
create a method on the first controller like addObjectToList(appendAct) and in that method, just easily append object to your list array and reload tableView after adding.
You will be reloading tableView only when you really add something to your list and not every time controller appears, you also don't need notification observer.
EDIT - UPDATE
What is this?
if newName == "" {
sceneLiveName.text = "Enter Something"
sceneNameTextField.text = ""
}
else {
let appendAct: Act = Act(actName: newName!, actTheme: "Action", actScenes: [Scene(sceneTitle: "Add Act", sceneDescription: "")])
shit.addObjects(appendage: appendAct)
dismiss(animated: true, completion: nil)
}
I mean - what is shit.AddObjects? Shit is defined as tableView - but you have to call this method on instance of your controller.
Another thing - change your setup from sections == number of items with 1 row to be one section with number of rows == number of items. :)
I have this tableView:
but I can't recognize when user selects the UISwitch in the row.
I know I need an #IBAction in the UITableViewCell class but I didn't found a guide that works for me.
I'd like to print the indexPath of the row when user clicks on the UISwitch.
This is my class for cell:
class TicketsFilterCell: UITableViewCell {
#IBOutlet weak var textFilter: UILabel!
#IBOutlet weak var switchFilter: UISwitch!
class var reuseIdentifier: String? {
get {
return "TicketsFilterCell"
}
}
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
How can I do?
Thanks in advance.
Edit
My cellForRowAtIndexPath:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell: TicketsFilterCell? = tableView.dequeueReusableCellWithIdentifier("cellTicketFilter", forIndexPath: indexPath) as? TicketsFilterCell
cell?.delegate = self
if(indexPath.section == 0) {
let text = status[indexPath.row]?.capitalizedString
cell?.textFilter.text = text
} else if(indexPath.section == 1) {
let text = queues [indexPath.row]?.capitalizedString
cell?.textFilter.text = text
} else if(indexPath.section == 2) {
let text = types[indexPath.row]?.capitalizedString
cell?.textFilter.text = text
} else if(indexPath.section == 3) {
let text = severities[indexPath.row]?.capitalizedString
cell?.textFilter.text = text
}
return cell!
}
Jigen,
Here is what you can do :)
Declare a protocol in your cell class lets call it as CellProtocol.
protocol CellProtocol : class {
func switchButtonTapped(WithStatus status : Bool, ForCell myCell : TicketsFilterCell)
}
Declare a variable in your TicketsFilterCell class to hold delegate,
weak var delegate : CellProtocol!
When user taps on your switch trigger the delegate as
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
self.delegate .switchButtonTapped(WithStatus: selected, ForCell: self)
}
In your TableViewController confirm to protocol using
class ViewController: UITableViewController,CellProtocol {
Now in your ViewController's cellForRoAtIndexPath, for each cell set the delegate as self.
cell.delegate = self
Finally implement the delegate method as follow
func switchButtonTapped(WithStatus status: Bool, ForCell cell: TicketsFilterCell) {
let indexPath = self.tableView .indexPathForCell(cell)
print("cell at indexpath \(indexPath) tapped with switch status \(status)")
}
EDIT :
For each cell you have dragged the IBOutlet for switch :) Now all you need is IBAction from UISwitch to the cell :)
#IBAction func switchTapped(sender: UISwitch) {
self.delegate.switchButtonTapped(WithStatus: sender.on, ForCell: self)
}
Thats it :)
Use this
cell.yourSwitch.tag=indexPath.row;
in cellForRowAtIndexPath method
Based on that tag perform your code in switch on/off action.
Add an #IBAction from the switch to the view controller itself. In the IBAction function convert the position of the sender(UISwitch) to tableView's frame. Then, find the cell at that point in tableView.
I believe this is the most elegant way to add actions to table view cells.
The biggest problem with the tag property is that its use always starts out simple, but over time the logic (or lack thereof) evolves into a fat unreadable mess.
This is easier than creating a protocol and implementing the delegate
Below is the IBAction connected to the switch's valueChanged action
#IBAction func switchValueChanged(sender: AnyObject) {
let switchButton = sender as! UISwitch
let buttonPosition = switchButton.convertPoint(CGPointZero, toView: self.tableView)
let indexPath = self.tableView.indexPathForRowAtPoint(buttonPosition)!
NSLog("switch at index %d changed value to %#", indexPath.row, switchButton.on)
}
I am trying to run an action for a button being pressed within a table view cell. The following code is in my table view controller class.
The button has been described as "yes" in an outlet in my class of UITableViewCell called requestsCell.
I am using Parse to save data and would like to update an object when the button is pressed. My objectIds array works fine, the cell.yes.tag also prints the correct number to the logs, however, I cannot get that number into my "connected" function in order to run my query properly.
I need a way to get the indexPath.row of the cell to find the proper objectId.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as requestsCell
// Configure the cell...
cell.name.text = requested[indexPath.row]
imageFiles[indexPath.row].getDataInBackgroundWithBlock{
(imageData: NSData!, error: NSError!) -> Void in
if error == nil {
let image = UIImage(data: imageData)
cell.userImage.image = image
}else{
println("not working")
}
}
cell.yes.tag = indexPath.row
cell.yes.targetForAction("connected", withSender: self)
println(cell.yes.tag)
return cell
}
func connected(sender: UIButton!) {
var query = PFQuery(className:"Contacts")
query.getObjectInBackgroundWithId(objectIDs[sender.tag]) {
(gameScore: PFObject!, error: NSError!) -> Void in
if error != nil {
NSLog("%#", error)
} else {
gameScore["connected"] = "yes"
gameScore.save()
}
}
}
Swift 4 & Swift 5:
You need to add target for that button.
myButton.addTarget(self, action: #selector(connected(sender:)), for: .touchUpInside)
And of course you need to set tag of that button since you are using it.
myButton.tag = indexPath.row
You can achieve this by subclassing UITableViewCell. Use it in interface builder, drop a button on that cell, connect it via outlet and there you go.
To get the tag in the connected function:
#objc func connected(sender: UIButton){
let buttonTag = sender.tag
}
The accepted answer using button.tag as information carrier which button has actually been pressed is solid and widely accepted but rather limited since a tag can only hold Ints.
You can make use of Swift's awesome closure-capabilities to have greater flexibility and cleaner code.
I recommend this article: How to properly do buttons in table view cells using Swift closures by Jure Zove.
Applied to your problem:
Declare a variable that can hold a closure in your tableview cell like
var buttonTappedAction : ((UITableViewCell) -> Void)?
Add an action when the button is pressed that only executes the closure. You did it programmatically with cell.yes.targetForAction("connected", withSender: self) but I would prefer an #IBAction outlet :-)
#IBAction func buttonTap(sender: AnyObject) {
tapAction?(self)
}
Now pass the content of func connected(sender: UIButton!) { ... } as a closure to cell.tapAction = {<closure content here...>}. Please refer to the article for a more precise explanation and please don't forget to break reference cycles when capturing variables from the environment.
Simple and easy way to detect button event and perform some action
class youCell: UITableViewCell
{
var yourobj : (() -> Void)? = nil
//You can pass any kind data also.
//var user: ((String?) -> Void)? = nil
override func awakeFromNib()
{
super.awakeFromNib()
}
#IBAction func btnAction(sender: UIButton)
{
if let btnAction = self.yourobj
{
btnAction()
// user!("pass string")
}
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = youtableview.dequeueReusableCellWithIdentifier(identifier) as? youCell
cell?.selectionStyle = UITableViewCellSelectionStyle.None
cell!. yourobj =
{
//Do whatever you want to do when the button is tapped here
self.view.addSubview(self.someotherView)
}
cell.user = { string in
print(string)
}
return cell
}
We can create a closure for the button and use that in cellForRowAtIndexPath
class ClosureSleeve {
let closure: () -> ()
init(attachTo: AnyObject, closure: #escaping () -> ()) {
self.closure = closure
objc_setAssociatedObject(attachTo, "[\(arc4random())]", self,.OBJC_ASSOCIATION_RETAIN)
}
#objc func invoke() {
closure()
}
}
extension UIControl {
func addAction(for controlEvents: UIControlEvents = .primaryActionTriggered, action: #escaping () -> ()) {
let sleeve = ClosureSleeve(attachTo: self, closure: action)
addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
}
}
And then in cellForRowAtIndexPath
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = youtableview.dequeueReusableCellWithIdentifier(identifier) as? youCell
cell?.selectionStyle = UITableViewCell.SelectionStyle.none//swift 4 style
button.addAction {
//Do whatever you want to do when the button is tapped here
print("button pressed")
}
return cell
}
class TableViewCell: UITableViewCell {
#IBOutlet weak var oneButton: UIButton!
#IBOutlet weak var twoButton: UIButton!
}
class TableViewController: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! TableViewCell
cell.oneButton.addTarget(self, action: #selector(TableViewController.oneTapped(_:)), for: .touchUpInside)
cell.twoButton.addTarget(self, action: #selector(TableViewController.twoTapped(_:)), for: .touchUpInside)
return cell
}
func oneTapped(_ sender: Any?) {
print("Tapped")
}
func twoTapped(_ sender: Any?) {
print("Tapped")
}
}
With Swift 5 this is what, worked for me!!
Step 1. Created IBOutlet for UIButton in My CustomCell.swift
class ListProductCell: UITableViewCell {
#IBOutlet weak var productMapButton: UIButton!
//todo
}
Step 2. Added action method in CellForRowAtIndex method and provided method implementation in the same view controller
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ListProductCell") as! ListProductCell
cell.productMapButton.addTarget(self, action: #selector(ListViewController.onClickedMapButton(_:)), for: .touchUpInside)
return cell
}
#objc func onClickedMapButton(_ sender: Any?) {
print("Tapped")
}
As Apple DOC
targetForAction:withSender: Returns the target object that responds to
an action.
You can't use that method to set target for UIButton.
Try
addTarget(_:action:forControlEvents:) method
Get cell like this, Hope it will help someone
#objc func addActionClicked(sender: UIButton) {
let buttonPosition: CGPoint = sender.convert(CGPoint.zero, to: trustedTableView)
let indexPath = trustedTableView.indexPathForRow(at: buttonPosition)
let cell = trustedTableView.cellForRow(at: indexPath!) as! addNetworkSSIDCell
}
in Swift 4
in cellForRowAt indexPath:
cell.prescriptionButton.addTarget(self, action: Selector("onClicked:"), for: .touchUpInside)
function that run after user pressed button:
#objc func onClicked(sender: UIButton){
let tag = sender.tag
}
The accepted answer is good and simple approach but have limitation of information it can hold with tag. As sometime more information needed.
You can create a custom button and add properties as many as you like they will hold info you wanna pass:
class CustomButton: UIButton {
var orderNo = -1
var clientCreatedDate = Date(timeIntervalSince1970: 1)
}
Make button of this type in Storyboard or programmatically:
protocol OrderStatusDelegate: class {
func orderStatusUpdated(orderNo: Int, createdDate: Date)
}
class OrdersCell: UITableViewCell {
#IBOutlet weak var btnBottom: CustomButton!
weak var delegate: OrderStatusDelegate?
}
While configuring the cell add values to these properties:
func configureCell(order: OrderRealm, index: Int) {
btnBottom.orderNo = Int(order.orderNo)
btnBottom.clientCreatedDate = order.clientCreatedDate
}
When tapped access those properties in button's action (within cell's subclass) that can be sent through delegate:
#IBAction func btnBumpTapped(_ sender: Any) {
if let button = sender as? CustomButton {
let orderNo = button.orderNo
let createdDate = button.clientCreatedDate
delegate?.orderStatusUpdated(orderNo: orderNo, createdDate: createdDate)
}
}
Let me offer another approach for getting the cell from a button within it.
The idea is to subclass UIButton only to open a weak pointer to a UITableViewCell.
class MyButton: UIButton {
#objc weak var cell: UITableViewCell?
}
In the UITableViewCell:
override func awakeFromNib() {
super.awakeFromNib()
myButton.cell = self
}
In the ViewController of the table view, where the button is connected:
#IBAction func myButtonAction(sender: MyButton) {
let parentCell = sender.cell
...
}