Is it save to access an outlet of a custom UITableViewCell right after instantiating it with dequeueReusableCellWithIdentifier?
E.g.
class MyCell: UITableViewCell {
#IBOutlet weak var myImageView: UIImageView!
var image: UIImage?
override func awakeFromNib() {
update()
}
func update() {
myImageView.image = image
}
}
class MyViewController: UIView() {
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("MyCellIdentifier") as! MyCell
cell.image = UIImage(...)
cell.update()
}
}
I have used this implementation a lot but very rarely (<0.001%) I get a crash report pointing to line myImageView.image = image.
UPDATE:
So far the crashes have been observed only for one specific implementation where 1 outlet is linked to many UIImageView() in custom cells because they share the same class.
The simple method dequeueReusableCellWithIdentifier: returns an optional which is not safe.
Use this method instead which is safe because it returns an non-optional cell
let cell = tableView.dequeueReusableCellWithIdentifier("MyCellIdentifier",
forIndexPath: indexPath) as! MyCell
Since the image property of an UIImageView object can be nil it's recommended to declare related UIImage properties as optional (?) rather than implicit unwrapped optional (!) without the default initializer (())
Related
Question
I'm creating a re-usable custom UITableViewCell and then subclassing it for re-use. How do I set these various subclasses to link to the same xib file?
Background
Let's call my custom UITableViewCell PickerTableViewCell. This cell includes a UIPickerView, as well as all the implementations as to how the picker view looks and behaves. When I want to use this cell, the only thing I need to give it is the data for the picker view. So I subclass PickerTableViewCell, then simply create the data source I need and assign it to the picker view. So far this has all worked well.
Here are the relevant parts of PickerTableViewCell:
class PickerTableViewCell: UITableViewCell {
var picker: UIPickerView! = UIPickerView()
var pickerDataSource: PickerViewDataSource!
override func awakeFromNib() {
super.awakeFromNib()
self.picker = UIPickerView(frame: CGRect(x: 0, y: 40, width: 0, height: 0))
self.picker.delegate = self
self.assignPickerDataSource()
}
// Must be overriden by child classes
func assignPickerDataSource() {
fatalError("Must Override")
}
Here is an example of a subclass:
class LocationPickerTableViewCell: PickerTableViewCell {
override func assignPickerDataSource() {
self.pickerDataSource = LocationPickerDataSource()
self.picker.dataSource = self.pickerDataSource
}
}
Problem
Since I am using these cells all over the place, with different data sources, I created a xib file which defines how the cell looks called PickerTableViewCell.xib, and assign it to the class PickerTableViewCell. In the view controllers I want to use it for, I register the cell with the table view inside viewDidLoad(). Then, inside func tableView(_:, cellForRowAt) I dequeue the subclass I want like this:
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier") as! LocationPickerTableViewCell
return cell
This is where the problem happens. The cell that is created is a PickerTableViewCell, not its subclass LocationPickerTableViewCell. This runs into the fatal error I placed in the parent class which is overriden by the child class.
The only way I have found to solve this is to create a separate xib file for each subclass I want to create, and assign it to the relevant subclass. While this solution does work, it feels wrong to have all of these xib files which are practically identical (except for which class they are assigned to) inside my project.
Is there a way I can overcome this problem, and have all of these cells link to the same single xib file?
Thanks! :)
Add view loaded by xib to UITableViewCell classes in which you want to use it.
Create your xib as per your require design, in your example PickerTableViewCell.xib
Create UITableViewCell sub-classes in which you want to use that view. I am using FirstTableViewCell & SecondTableViewCell for this.
in constructor of table cell load the xib and add it to table cell.
let nib = Bundle.main.loadNibNamed("PickerTableViewCell", owner: nil, options: nil)
if let view = nib?.first as? UIView{
self.addSubview(view)
}
if xib have any #IBOutlet then get them by viewWithTag function and assign to class variables
if let label = self.viewWithTag(1) as? UILabel{
self.label = label
}
override reuseIdentifier var of each tableviewCell subclass with different name
override var reuseIdentifier: String?{
return "FirstTableViewCell"
}
Now You can use these classes where you want, for using this follow below steps:
register this tableviewCell subclass with xib with tableview:
tableView.register(FirstTableViewCell.self, forCellReuseIdentifier:"PickerTableViewCell")
now in cellForRowAt indexPath method use it.
var cell = tableView.dequeueReusableCell(withIdentifier: "FirstTableViewCell", for: indexPath) as? FirstTableViewCell
if cell == nil {
cell = FirstTableViewCell()
}
cell?.label?.text = "FirstTableViewCell"
Don't use subclassing to assign different data sources.
Approach 1: Assign pickerDataSource in tableView(_:cellForRowAt:)
In the table view controller, you need to assign pickerDataSource
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier") as! PickerTableViewCell
cell.pickerDataSource = LocationPickerDataSource()
return cell
}
Handle additional work needed after the assignment of pickerDataSource with a didSet.
class PickerTableViewCell: UITableViewCell {
var picker: UIPickerView! = UIPickerView()
var pickerDataSource: PickerViewDataSource! {
didSet {
self.picker.dataSource = self.pickerDataSource
}
}
…
}
Approach 2: Extend PickerTableViewCell in all the needed ways.
Here instead of subclassing add the needed logic to a uniquely named setup method each defined in their own extension.
extension PickerTableViewCell {
func setupLocationPickerDataSource() {
self.pickerDataSource = LocationPickerDataSource()
self.picker.dataSource = self.pickerDataSource
}
}
then in tableView(_:cellForRowAt:)
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier") as! PickerTableViewCell
cell.setupLocationPickerDataSource()
return cell
}
I am trying to pass in the String "random text" to equal the property imageURL that is located inside of the custom Cell UserCell from CellForRowAtIndexPath. In didSet of the cell class, I can print "random text" just fine, however, for reasons unrelated to this question, I need to print "random text" inside of the lazy var. When I try to print it inside the lazy var, or even awakeFromNib, it gives me nil. I have an idea of why this is happening. I'm assuming the compiler runs certain code for a custom cell class before initializing any self properties. I'm wondering if there is a way to get around that.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(cellId, forIndexPath: indexPath) as! UserCell
cell.imageURL = "random text"
return cell
}
Inside of the custom cell (UserCell)
class UserCell: UITableViewCell {
var imageURL: String? {
didSet{
print(imageURL) //prints the imageURL
}
}
lazy var profileImageView: UIImageView = {
let imageView = UIImageView()
print(imageURL) /////prints nil
return imageView
}()
My code:
import Foundation
import Firebase
class CellOneViewController: UITableViewCell {
#IBOutlet weak var new1: UILabel!
let ref = Firebase(url: "https://burning-heat-8250.firebaseio.com/slide2")
func viewdidload() {
ref.observeEventType (.Value, withBlock: { snapshot in
self.new1.text = snapshot.value as? String
})
}
}
I've read around that you can't call viewDidLoad in a UITableViewCell, only in a UITableViewController. All the answers are in Objective-C, but I'm writing the app in Swift. I don't receive any critical errors but when running the app nothing appears in the cell where the label is. I'm fairly new to using Xcode, as I am just going around following guides so if I'm saying something incorrect let me know.
I think, you need method func layoutSubviews().
Only ViewController gets func viewDidLoad() called, after view is loaded.
If you need to initialize something or update views, you need to do in layoutSubviews(). As soon, your view or UITableViewCell gets loaded, layoutSubviews() get called.
Replace viewDidLoad with layoutSubviews()
class CellOneViewController: UITableViewCell {
#IBOutlet weak var new1: UILabel!
let ref = Firebase(url: "https://burning-heat-8250.firebaseio.com/slide2")
override func layoutSubviews() {
super.layoutSubviews()
ref.observeEventType (.Value, withBlock: { snapshot in
self.new1.text = snapshot.value as? String
})
}
}
The reason you can't do this is that a UITableViewCell isn't a subclass of UIViewController.
The cellForRowAtIndexPath method in the UITableViewDataSource is where you should set up your cells. You probably only want to do something like this in your custom UITableViewCell:
class CellOne: UITableViewCell {
#IBOutlet weak var new1: UILabel!
}
Then in your TableViewController's cellForRowAtIndexPath method (provided you have imported firebase) you should dequeue a reusable cell, cast it as a CellOne (as! cellOne) and then you can set the new1.text value. I don't know what your reuse identifier is, but it would look something like this:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Your-Reuse-Identifier", forIndexPath: indexPath) as! CellOne
cell.new1.text = "Your Value"
return cell
}
I have gone through all the questions regarding this matter that seems to be popular. Anyhow, I have created a simple app with a table view that uses custom cells.
I use the storyBoard, and defined the cells with the same name the class I created, Xcode even auto-completed me.
Though when I initialise a new cell, I can't change the properties of the labels and image contained in the cell. I receive an error saying I accessed a nil, and I kinda get it. Though I couldn't seem to find a way around it. Can someone help?
import UIKit
class ExtenderCell: UITableViewCell {
#IBOutlet var main_image: UIImageView!
#IBOutlet var name_label: UILabel!
#IBOutlet var desc_label: UILabel!
init(to_put_image: UIImage, name: String, desc:String){
super.init(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "cell")
name_label.text = name
desc_label.text = desc
main_image.image = to_put_image
}
}
You are using storyBoard for cell creation with outlets.Your outlets are not accessible to you till awakeForNib() using storyBoard as it is not unarchived till awakeFromNib. Your outlets or properties are nil in init so you are getting this exception as you are trying to unwrap nil outlet property in init method.
As your outlet properties are not accessible in init method and they are nil in init.So you need to set your outlets in awakeForNib().Or you can set the properties in cellForRowAtIndexPath.So best approach is to make your init method as instance method if you want to use storyBoard.
import UIKit
class ExtenderCell: UITableViewCell {
#IBOutlet var main_image: UIImageView!
#IBOutlet var name_label: UILabel!
#IBOutlet var desc_label: UILabel!
func setContents(to_put_image: UIImage, name: String, desc:String){
name_label.text = name
desc_label.text = desc
main_image.image = to_put_image
}
}
set the contents of cell in cellForRowAtIndexPath
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
//Check your identifier in storyBoard is "cell"
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as ExtendedCell
cell.setContents(yourImage, name: yourName, desc: yourDescription)
return cell
}
I've a table view that has a custom table view cell in it. My problem is that when I try and assign a value to a variable in the custom UITableViewCell I get the stated error. Now, I think its because the said variable is not initialised, but it got me completely stumped.
This is the custom table cell:
import Foundation
import UIKit
class LocationGeographyTableViewCell: UITableViewCell
{
//#IBOutlet var Map : MKMapView;
#IBOutlet var AddressLine1 : UILabel;
#IBOutlet var AddressLine2 : UILabel;
#IBOutlet var County : UILabel;
#IBOutlet var Postcode : UILabel;
#IBOutlet var Telephone : UILabel;
var location = VisitedLocation();
func Build(location:VisitedLocation) -> Void
{
self.location = location;
AddressLine1.text = "test";
}
}
My cell for row at index path is:
override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell!
{
var addressCell = tableView.dequeueReusableCellWithIdentifier("ContactDetail") as? LocationGeographyTableViewCell;
if !addressCell
{
addressCell = LocationGeographyTableViewCell(style: UITableViewCellStyle.Value1, reuseIdentifier: "ContactDetail");
}
addressCell!.Build(Location);
return addressCell;
}
As I say I'm completely baffled, the Build function calls the correct function in the UITableViewCell.
Any help will be gratefully appreciated.
Ta
I just made a simple project with your code, and I have a nil "AddressLine1" label, which causes the same error you have. I assume we have the same problem.
I solved it by adding the identifier "ContactDetail" to the prototype cell in my storyboard.
I also suggest that you change your code a little :
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// This newer API ensures that you always get a cell, so no need for optionals.
// Also note the "let" that is preferred since you don't plan on changing addressCell
let addressCell = tableView.dequeueReusableCellWithIdentifier("ContactDetail", forIndexPath: indexPath) as LocationGeographyTableViewCell;
//addressCell.Build(Location); // the cell is a view, views should not know about model objects
addressCell.AddressLine1 = Location.addressLine1
// ... same for all labels, or do all that in a "configureCell" function
return addressCell;
}
I finally solved it. Instead of using the storyboard to define the UITableViewCell (as I'd done in the parent view controller) I created a Xib with the content and wired that up to the cell's class, referenced it in the TableViewController and allocated it in the cell created method.
viewDidLoad()
{
var nib = UINib(nibName: "LocationTableViewCell", bundle: nil)
tableView.registerNib(nib, forCellReuseIdentifier: "locationCell")
}
var cell:LocationGeographyTableViewCell = self.tableView.dequeueReusableCellWithIdentifier("locationCell") as LocationGeographyTableViewCell
cell.AddressLine1.text = "teetetette";
return cell;