I have been stuck for hours on this issue. I have created a custom cell inside a tableview which is inside a UIVC. The custom cell has it's own class file and is all linked up as well as having the buttons inside it connected to the UIVC via delegates.
Inside the custom cell there consists of a UITextView as well as some buttons.
Tapping the tableview adds a new custom cell. The user can type whatever their beautiful heart desires, eventually resigning the first responder of the textview. Here is my issue(s).
MAIN. First off. Overtime, the cells start mixing up the text that was typed in by the user, and starts reusing it. It becomes a mess.
Due to the fact I have a custom cell, I do not need to register the class in the viewDidLoad of the UIVC. A stack overflow answer stated.
Optional. Second. Eventually, after a certain point the keyboard blocks the cell view. I have no idea how to scroll the cell to the top of the tableview.
Optional. Third. This is just a bonus. How would one be able to save the data from a cell with a decent storage method. I heard NSUserDefaults is only good for small memory storage files. Basically. Transferring the data in the textview inside the custom cell, into another area (UIVC?) where the data can be saved. Most likely using an array and saving the data on that array.
Code. As requested.
UIVC
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let reminderCell = reminder_tableView.dequeueReusableCellWithIdentifier("cellReminder", forIndexPath:indexPath) as! addReminderCell
reminderCell.selectionStyle = .None
reminderCell.heightForReminderTextDelegate = self
reminderCell.delegate = self
reminderCell.reminder_textview.becomeFirstResponder()
return reminderCell
}
Custom Cell Code. Not all of it though.
import UIKit
import AVFoundation
//MARK: Height For Reminder Delegate
protocol HeightForReminderTextView
{
func heightOfTextView(height: CGFloat)
}
//MARK: Beginning of Class
class addReminderCell: UITableViewCell, UITextViewDelegate {
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: nil)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
//MARK: Delegate Declarations
var heightForReminderTextDelegate :HeightForReminderTextView?
//MARK: Element IBOutlets
#IBOutlet var reminder_textview: UITextView!
#IBOutlet var reminder_cell_uiview: UIView!
//MARK: Button
#IBAction func non_priority_button(sender: AnyObject) {
println("pressed")
}
//MARK: Awake From Nib Starts Here
override func awakeFromNib() {
super.awakeFromNib()
reminder_textview.delegate = self
}
MAIN: When you dequeue a cell it means that cell may be reused it does not create a new cell everytime. For that UITableViewCell has a method named prepareForReuse() which you can override, where you can reset all your cell content. Ex:
override func prepareForReuse() {
myTextView.text = ""
}
Due to the fact I have a custom cell...
If you have only *.swift file for your custom cell you must registerClass(:forCellReuseIdentifier:)
If you also have *.xib file you should registerNib(:forCellReuseIdentifier:) in the viewDidLoad
Optional
You can use scrollToRowAtIndexPath(_:atScrollPosition:animated:) to scroll desired cell to top of tableView
Optional
You can use CoreData, JSON, some SQLite wrappers to save data. In NSUserDefaults you can use small amount of data, like some settings.
I recommend to use NSJSONSerialization for now. You can create NSData from a Dictionary and write data as *.json to Documents directory using NSFileManager. In your app you should keep an array or dictionary with all the data and pass to another view controller that data.
when user types text, you should save it to some array or dictionary accoding to your requirment and that would helpyou with resusing problem.
Now, second problem of yours is scrolling I answered this question hope this will help. this is in objective C although.
and third for transferring data from OneVc to another you can use Segues
Hope this helps
Related
I am new to iOS/Swift development, and having a problem with making a dynamic swap of DataSource work for a UITableView - note I am not swapping the Delegate, just the DataSource.
I have read other similar questions/responses on Stack Overflow, and not found one that's relevant to my situation. Typically they're about setting the DataSource on "viewDidLoad" (e.g. this one, and this one), whereas my situation is about swapping the DataSource when the user presses a button. The problems in the referenced questions don't exist in my code.
Here's outline of my code. I have the buttonPress method connected to the TouchUpInside event in the storyboard:
class ViewController: UIViewController, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
...
#IBAction func buttonPress(sender: UIButton) {
...
self.tableView.dataSource = DummyDataSource()
self.tableView.delegate = self
self.tableView.reloadData()
}
...
}
...and here's my datasource class:
import UIKit
class DummyDataSource: NSObject, UITableViewDataSource {
let names = ["A", "B", "C"]
func tableView(tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return names.count
}
func tableView(tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier(simpleTableIdentifier) as UITableViewCell?
if ( cell == nil ) {
cell = UITableViewCell( style: UITableViewCellStyle.Default,
reuseIdentifier: simpleTableIdentifier)
}
cell!.textLabel?.text = names[indexPath.row]
return cell!
}
}
When I press the button, I can see that the pressButton method is being called correctly, but the data doesn't show up in the tableView (no errors - just no data). Any ideas please? Thank you.
UITableView's dataSource property is either unsafe_unretained or weak, depending on which version of iOS. Either way, as with any other delegate, it doesn't keep a strong reference.
So when you write a line like this:
self.tableView.dataSource = DummyDataSource()
Your newly instantiated DummyDataSource() property doesn't have any strong references pointing to it. It is therefore immediately released by ARC.
We need to keep a strong reference to the data source if we want it to stick around.
My recommendation would be to add a data source property to your view controller which can keep the strong reference. We will also use the didSet of this property to set the table view's data source property and reload its data.
var dataSource: UITableViewDataSource? {
didSet {
tableView?.dataSource = dataSource
tableView?.reloadData()
}
}
We use optional-chaining to protect against the data source being set before the view is loaded and the tableView property is populated. Otherwise, we will get a fatal error for trying to unwrap nil.
We shouldn't need to be setting the data source property on the table view anywhere else. And the only reason why we should need to called reloadData() anywhere else is if our data source itself can change the data it is representing. However, it is important that reloadData() is called in sync with resetting the dataSource to protect against some likely index-out-of-bound crashes.
I use a UITableView inside a UIViewController with custom cells. However, when dequeueReusableCellWithIdentifier is called, it sometimes creates a new cell instead of reusing an existing one.
For example: When the first cell gets updated for the first time, a new cell is created instead of reusing the original cell.
I have created a diagram to help better illustrate the problem. Each green rectangle is an action that should happen, and each red rectangle is one that shouldn't. Each arrow is a call of dequeueReusableCellWithIdentifier.
It's problematic when a UIButton is tapped (the action is called multiple times), and with a "swipe to delete" action, the cell which has been swiped can be covered by the new second cell, which makes it appear like the delete button is randomly disappearing.
How can I solve this problem?
EDIT:
code custom cell
class TableViewCell : UITableViewCell{
#IBOutlet weak var buttonStop: UIButton!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
print("newCell")
backgroundColor = color
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
override func prepareForReuse() {
super.prepareForReuse()
}
var color = UIColor().randomColor()
}
code for extension randomColor
extension UIColor {
func randomColor() -> UIColor {
let aRedValue = CGFloat(arc4random()) % 255 / 255
let aGreenValue = CGFloat(arc4random()) % 255 / 255
let aBlueValue = CGFloat(arc4random()) % 255 / 255
return UIColor(red: aRedValue, green: aGreenValue, blue: aBlueValue, alpha: 1)
}
code for cellForRow
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCellWithIdentifier("Cell",forIndexPath: indexPath) as! TableViewCell
cell.buttonStop.tag = indexPath.row
cell.buttonStop.addTarget(self, action: "buttonAction:", forControlEvents: UIControlEvents.TouchUpInside)
return cell
}
buttonAction:
func buttonAction(sender: UIButton) {
print("action for cell : \(sender.tag)")
}
I reduced the code to the max , note after further investigation the button printed the same text because of one of my mistake I write the message twice ^^'
So the only problem remaining is for the "swipe to delete" gesture
If I understand correctly you have a problem with keeping (the result of) an action in sync with what is displayed in the tableView.
Understand how UITableView uses the cells: it will ask it's dataSource for a UITableViewCell. You can either create one every time, or ask the tableView for one to reuse. Because of this mechanism, a cell you create can appear in a certain position at one time, and another position later.
Make sure that your logic and data is independent of the actual tableViewCells at all times. So you should be able to do what you have to o with ONLY an NSIndexPath.
A UITableViewCell can give you a valid indexPath for a tableViewCell via one of two method -indexPathForCell: or indexPathForRowAtPoint:.
The way to handle actions on UITableViewCells is as follows:
Create a custom subclass of UITableViewCell
Create a protocol in this subclass (e.g. MyCellDelegate)
Give this class a property delegate of type id <MyCellDelegte>
When creating a cell in your dataSource, sign this dataSource to be the delegate of that cell.
Let this subclass handle the action (swipe / tap) by calling a method from your protocol on it's delegate (the delegate is your datasource).
In the implementation of that delegate method (on your datasource) you can call one of the methods on UITableView to get the indexPath. This is the point where you become independent on the actual tabelViewCell: use that indexPath to get data, or perform a certain operation.
Although this might seem like a lot of work for a small thing, it really is not that much work, and your code will be much more robust, easier to follow and less coupled.
nomrally i have one view controller and i add outlets to it. and inside view did load i can change the font of the text view or i can add images to the ui image. but now i have a custom cell like this:
class NumberOfPeopleTableViewCell: UITableViewCell {
#IBOutlet weak var numberOfPeopleLabel: UILabel!
as you see, i have outlet in that cell, how can i interact with that outlet? there is no view did load function inside the table view cell
You could set the font of the label in your storyboard.
Or you could set the font of the label in awakeFromNib (a method you can override in your cell class).
Or you could set the font of the label in tableView(_:cellForRowAtIndexPath:) (in your table view controller).
From what I can gather, the "standard" approach would be, inside your UITableViewController subclass:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("<Your Id Here>", forIndexPath: indexPath) as! NumberOfPeopleTableViewCell
let foo = cell.numberOfPeopleLabel // DO STUFF WITH IT
// Do more stuff here
return cell
}
OR --
If you want to keep your View Controller code clean of graphics layout-ing / appearance logic (which you should) consider, inside NumberOfPeopleTableViewCell to override func layoutSubviews().
OR (my favourite) --
You could subclass the UITextView (e.g. MYCellTextView) and change the appearance for all of your subclass instances in your app delegate using UIApparence. Like this should work:
MyCellTextView.appearance().font = UIFont(name: "<myFont>", size: 10.0)
You can put the above, for example, in your app delegate. This has the advantage of keeping appearance code in one place and away from view controllers (and generic, for all instances of your custom view).
You can override the function layoutSubviews
override func layoutSubviews() {
//do whatever here for your outlets
}
take a look at awakeFromNib. as the documentation states:
When an object receives an awakeFromNib message, it is guaranteed to have all its outlet instance variables set.
so you can do:
override func awakeFromNib() {
super.awakeFromNib()
numberOfPeopleLabel.font = UIFont.systemFontOfSize(15.0) // or whatever font you want to assign
}
In my iOS app I have a UITableViewController with many cells and some of them contain a UITextField so that the user can insert text. The "problem" is that if, after entering the text in the right cell, the user scrolls down the table and then scrolls up it, then those cells are initialized again and the text il blank again. Is there a way to prevent this? So that the cells are initialized only when the table is scrolled down... Thank you
With reference to my comment to the initial code and code provided.
Many developers implement UITableViewDatasource methods in a natural way.
Put inside cellForRowAtIndexPath a conditional switch/case block and dependent on indexPath.row dequeue and return reusable cell. This is ok, but this also means that you need to put one more switch/case block into heightForRowAtIndexPath and probably into numberOfRowsInSection and didSelectRowAtIndexPath.
What if you are in need of changing the order of cells in tableview, or in need of inserting new cell in between existing cells or something else. This cause to the significant rewrite of the existing code.
The good practice for the the predefined tableviews (known amount and order of rows) is to create an Object describing tableview cells and access it from all those UITableViewDatasource, UITableViewDelegate methods. This Object also could be used for storing the value of the dynamic elements in cells and could be updated accordingly.
Let say it's an array of NSDictionary's (mutable once if we decide to store data inside).
let cellsData : [[String: String]] = [["type": "TextField","value": ""], ["type": "DatePicker","value": ""], ["type": "TextField","value": "Default Value"]]
In your UITableViewDatasource, UITableViewDelegate method you access your cellsData array using indexPath.row get the NSDictionary object parse it, and dependent on the value of "type" key, dequeue proper cell. You will also have to switch/case block everywhere, but you check the value from cellsData Object not indexPath.row itself. So you can update you tableview, just by updating cellsData object. You can also put height into it etc.
I see you keep references from cells dequeued in tableview, for accessing them to retrieve data, but it's not needed if you have all actual data stored in the cellsData Object.
How to achieve it? Listen to events inside cell. Create custom events, and trigger them from cell.
To create custom event, you will need to create your custom delegate class (Reference Implementing a Delegate for a Custom Class)
The Swift concept is very similar to Objective-C. First we need to create the delegate protocol class:
protocol BigTextCellDelegate {
func controller(controller: BigTextCell, textFieldDidEndEditingWithText: String, atIndex: Int)
}
Then you need to declare a delegate property inside your custom cell class.
var delegate: BigTextCellDelegate?
Now you need to implement it in your tableview class, first make your tableview class conforming to BigTextCellDelegate protocol in a way.
class DetailsNewTaskViewController: UITableViewController, BigTextCellDelegate {
...
}
Then we need to implement BigTextCellDelegate, just place a method inside DetailsNewTaskViewController.
func controller(controller: BigTextCell, textFieldDidEndEditingWithText: String, atIndex: indexRow) {
// #todo: update `cellsData` at indexRow with `textFieldDidEndEditingWithText` text
}
Now you need to make your custom cell to listed to UITextFieldDelegate events and trigger our custom BigTextCellDelegate event.
First you need to make it conforming to UITextFieldDelegate protocol and then assign it during the initialization, you can achieve the same from Storyboard.
class BigTextCell: UITableViewCell, UITextFieldDelegate {
#IBOutlet weak var textField: UITextField!
var Int: rowIndex
var delegate: BigTextCellDelegate?
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
textField.delegate = self
}
func textFieldDidEndEditing(textField: UITextField) {
delegate?.controller(self, textFieldDidEndEditingWithText: textField.text, atIndex: rowIndex)
}
}
The delegate property of type BigTextCellDelegate is optional as we can't be sure it's not nil.
Now you need to assign your tableview to listen to BigTextCellDelegate events.
At cellForRowAtIndexPath, put:
cell = tableView.dequeueReusableCellWithIdentifier(...)
cell.delegate = self
cell.rowIndex = indexPath.row
// #todo: get data from cellsData array and assign it back to cell.textField.text
That's all.
As for the DatePicker and other cells. You can do it exactly in the same way.
Assign to valueChanged of DatePicker from inside the cell and trigger the method on new custom delegate protocol.
datePicker.addTarget(self, action: Selector("dataPickerChanged:"), forControlEvents: UIControlEvents.ValueChanged)
func datePickerChanged(datePicker:UIDatePicker) {
// #todo: trigger event
}
I am trying to create a custom cell for my UITableView but I am having some difficulty.
First off I cannot use the Interface Builder, as I am experiencing a variation on this bug in Xcode. Every time I click on an element in the Interface Builder everything in that view gets a height and width of zero and gets repositioned outside of the view. Besides, I would like to learn how to do this programmatically.
Secondly I am using the Swift language for my project. I have been trying to follow this demonstration, and doing my best to convert the Objective C code over to Swift, but whenever I run into problems I end up being stuck. I presume this is because I am not converting the code over correctly.
Thirdly I found this video but despite being fairly difficult to follow (lots of the code is just copied and pasted without much explanation to what it does or why), it still ends up using the Interface Builder to change various parts.
I have a basic UITableView set up fine. I just want to be able to add a custom cell to that table view.
Can this be done using pure programming, or do I need to use the Interface Builder?
Can anyone point me in the right direction or help me out in creating a custom cell programmatically in Swift?
Many thanks.
In general: Everything is possible in pure programming ;-)
Create a custom class for your tableView cell and there setup all the elements, properties and the visual layout. Implement the required methods init(style,reuseidentifier)
In your custom class for the UITableViewController register the custom cell class using registerClass(forCellReuseIdentifier)
Setup your delegate and datasource for the custom tableViewController
Finally, you create the cells in cellForRowAtIndexPath:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("myReuseIdentifier", forIndexPath: indexPath) as MyCustomTableViewCell
// configure the cell using its properties
return cell
}
This should be the basic steps.
If you're looking for more code, here is an example of a custom cell that I created:
// File: vDataEntryCell.swift
import UIKit
class vDataEntryCell: UITableViewCell
{
//-----------------
// MARK: PROPERTIES
//-----------------
//Locals
var textField : UITextField = UITextField()
//-----------------
// MARK: VIEW FUNCTIONS
//-----------------
///------------
//Method: Init with Style
//Purpose:
//Notes: This will NOT get called unless you call "registerClass, forCellReuseIdentifier" on your tableview
///------------
override init(style: UITableViewCellStyle, reuseIdentifier: String!)
{
//First Call Super
super.init(style: style, reuseIdentifier: reuseIdentifier)
//Initialize Text Field
self.textField = UITextField(frame: CGRect(x: 119.00, y: 9, width: 216.00, height: 31.00));
//Add TextField to SubView
self.addSubview(self.textField)
}
///------------
//Method: Init with Coder
//Purpose:
//Notes: This function is apparently required; gets called by default if you don't call "registerClass, forCellReuseIdentifier" on your tableview
///------------
required init(coder aDecoder: NSCoder)
{
//Just Call Super
super.init(coder: aDecoder)
}
}
Then in my UITableViewController class I did the following:
// File: vcESDEnterCityState.swift
import UIKit
class vcESDEnterCityState: UITableViewController
{
//-----------------
// MARK: VC FUNCTIONS
//-----------------
///------------
//Method: View Will Appear
//Purpose:
//Notes:
///------------
override func viewWillAppear(animated: Bool)
{
//First Call Super
super.viewWillAppear(animated)
//Register the Custom DataCell
tvCityStateForm.registerClass(vDataEntryCell.classForCoder(), forCellReuseIdentifier: "cell")
}
//-----------------
// MARK: UITABLEVIEW DELEGATES
//-----------------
///------------
//Method: Cell for Row at Index Path of TableView
//Purpose:
//Notes:
///------------
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
//Get Reference to Cell
var cell : vDataEntryCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as vDataEntryCell
//...Do Stuff
//Return Cell
return cell
}
}