Reusing cells in UITableView - ios

I have my custom cell 'NewsCell'. It contains my custom view 'ImageMosaicView' (that is just subclass of UIView). I use it to show photos like in post in Facebook. I just pass images' urls to ImageMosaicView's instance, and it loads it and shows.
I have an issue. When I scroll my tableView fast, then images from previous cell appear in new cell for a moment, while new images are loading. But I have no idea how they appear there, because I provided default images. Here is an example
How can I avoid this?
// ImageMosaicView.swift
class ImageMosaicView: UIView {
#IBOutlet var imageViews: [UIImageView]!
var urlStrings: [String] = [] {
didSet {
for imageView in imageViews {
if let url = URL(string: urlStrings[i]) {
imageView.loadImage(url: url)
}
}
}
// MARK: - Initialization
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let _ = loadViewFromNib()
}
// MARK: - Methods
func loadViewFromNib() -> UIView {
let bundle = Bundle.init(for: type(of: self))
let nib = UINib(nibName: "ImageMosaicView", bundle: bundle)
let view = nib.instantiate(withOwner: self, options: nil)[0] as! UIView
view.frame = bounds
view.autoresizingMask = [
UIViewAutoresizing.flexibleWidth,
UIViewAutoresizing.flexibleHeight
]
addSubview(view)
return view
}
}
// How I provide urls for images:
// NewsViewController.swift. 'tableView(cellForRow:, indexPath:)
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let news = newsCollection[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "NewsCell", for: indexPath) as! NewsCell
//...
cell.imageMosaicView.urlStrings = news.imageUrlsCollection
//...
return cell
} else {
return UITableViewCell()
}
}

The right way to do this is to configure the prepareForReuse() method inside the NewsCell class.
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = //Default image
//Whatever other things you need cleared.
}
This method is called whenever a tableView dequeues a cell. So any cached data should be cleared here or it will persist in the dequeued cell unless you manually change it before returning it in the cellForRowAt method.

Related

Using custom XIB in table view cell

I have followed this tutorial to create a custom .xib, which I plan to use in a table view's cell:
https://medium.com/#brianclouser/swift-3-creating-a-custom-view-from-a-xib-ecdfe5b3a960
Here is the .xib's class I created:
class UserView: UIView {
#IBOutlet var view: UIView!
#IBOutlet weak var username: UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
private func initialize() {
Bundle.main.loadNibNamed("UserView", owner: self, options: nil)
addSubview(view)
view.frame = self.bounds
view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
}
}
Previously, I was creating my table view cell within the storyboard, but I've come to realize that I want a more flexible view so that I can use it in different parts of my app, so I created the above custom .xib, UserView.
I have updated the table view cell in the storyboard to use the custom .xib:
https://i.stack.imgur.com/t7Tr7.png
Here is what my table view controller class looked like prior to creating the custom .xib (i.e. making the layout in the storyboard):
class UserTableViewController: UITableViewController {
// MARK: Properties
let provider = MoyaProvider<ApiService>()
var users = [User]()
override func viewDidLoad() {
super.viewDidLoad()
tableView.estimatedRowHeight = 100
tableView.rowHeight = UITableViewAutomaticDimension
// Fetch the user by their username
provider.request(.getUsers()) { result in
switch result {
case let .success(response):
do {
let results = try JSONDecoder().decode(Pagination<[User]>.self, from: response.data)
self.users.append(contentsOf: results.data)
self.tableView.reloadData()
} catch {
print(error)
}
case let .failure(error):
print(error)
break
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "UserTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? UserTableViewCell else {
fatalError("The dequeued cell is not an instance of UserTableViewCell.")
}
let user = users[indexPath.row]
cell.username.text = user.username
return cell
}
}
Here is the table view cell class:
class UserTableViewCell: UITableViewCell {
//MARK: Properties
#IBOutlet weak var userView: UserView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
My question is, how do I update the above table view controller class to use my custom .xib, instead of using the storyboard layout?
You can use 2 ways:
Create UITableViewCell (better)
1) Change UIView to UITableViewCell
class CustomTableViewCell: UITableViewCell {
...
class var identifier: String {
return String(describing: self)
}
}
2) Register your cell
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.registerNib(UINib(nibName: CustomTableViewCell.identifier, bundle: nil), forCellReuseIdentifier: CustomTableViewCell.identifier)
...
}
3) Use cellForRow(at:)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.identifier) as! CustomTableViewCell
cell.username.text = user.username
return cell
}
OR Add view as subview to cell (only in rare cases)
1) Add this to UserView
class UserView: UIView {
...
class func fromNib() -> UserView {
return UINib(nibName: String(describing: self), bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! UserView
}
}
2) Use cellForRow(at:)
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "UserTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? UserTableViewCell else {
fatalError("The dequeued cell is not an instance of UserTableViewCell.")
}
let userView = UserView.fromNib()
let user = users[indexPath.row]
userView.username.text = user.username
//Use frame size, but for me better to add 4 constraints
userView.frame = CGRect(x: 0, y: 0, width: cellWidth, height: cellHeight)
cell.contentView.addSubview(UserView)
return cell
}

Update content of user defined UIView inside UICollectionViewCell

I have UICollectionView with cells, that contains UIScrollView and inside it, there is a user defined UIView (via XIB)
I am using this code to init cell with data:
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
for v in cell.contentView.subviews {
if let scroll: UIScrollView = v as? UIScrollView {
scroll.contentOffset = CGPoint(x: 0, y: 0)
for s in scroll.subviews {
if let content: DetailedView = s as? DetailedView {
content.fillData()
}
}
}
}
}
In my DetailedView, I have:
#IBDesignable
class DetailedView: UIView {
#IBOutlet weak var btnTemp: UIButton!
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
// Performs the initial setup.
private func setupView() {
let view = viewFromNibForClass()
view.frame = bounds
// Auto-layout stuff.
view.autoresizingMask = [
UIViewAutoresizing.flexibleWidth,
UIViewAutoresizing.flexibleHeight
]
addSubview(view)
}
// Loads a XIB file into a view and returns this view.
private func viewFromNibForClass() -> UIView {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: String(describing: type(of: self)), bundle: bundle)
let view = nib.instantiate(withOwner: self, options: nil).first as! UIView
return view
}
func fillData(){
self.btnTemp.titleLabel?.text = "btn temp"
print("fill data")
}
#IBAction func btnTempClick(_ sender: Any) {
print("Click")
}
}
All is working - view is visible, btnTempClick is called, but fillData is not working. It does not change content of button, there is still default text "Button". How to fix this?
Just to make it official removing it from comment and putting it as an answer.
have you tried using self.btnTemp.setTitle("btn temp", for:.normal)?
i think you can try with setTitle forControlState

objc custom view with nib not visible on tableviewcell

I have an UITableViewCell contain a custom view, the custom view contains two label. The view hierarchical likes that:
|---Label 1
XIB---MyTableViewCell---MyView---|
|---Label 2
But run application, just shown 'MyView', Label 1 and Label 2 not visible! If I wrote code on viewDidLoad, take 'MyView' as viewController.view's subview, the label 1 and 2 is appeared. Hope you can help me.
Have you tried this?
Create MyView like this:
class MyView: UIView {
#IBOutlet weak var _label1: UILabel!
#IBOutlet weak var _label2: UILabel!
// Our custom view from the XIB file
var view: UIView!
//MARK: - Setup -
private func xibSetup() {
view = loadViewFromNib()
view.frame = bounds
// Make the view stretch with containing view
view.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
addSubview(view)
}
private func loadViewFromNib() -> UIView {
let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: "MyView", bundle: bundle)
let view = nib.instantiateWithOwner(self, options: nil)[0] as! UIView
return view
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
xibSetup()
}
override func awakeFromNib() {
super.awakeFromNib()
}
func setLabeles(lbl1: String, lbl2: String) {
_label1!.text = lbl1
_label2!.text = lbl2
}
}
Make Label1 and Label2 outlet to respective ones.
Then add it to custom tableViewCell as:
(Pink colour is for highlighting!)
Make My View outlet.
Custom cell code:
#IBOutlet weak var _myView: MyView!
class func identifier() -> String {
return "CustomTableViewCellId"
}
class func nib() -> UINib {
let nib = UINib(nibName: "CustomTableViewCell", bundle: NSBundle.mainBundle())
return nib
}
//MARK: Public Methods
func setCellIndexLabel(index: Int) {
_myView!.setLabeles("lbl1: \(index)", lbl2: "lbl2: \(index)")
}
Then there is no need to do extra anything in table view, just do:
In viewDidLoad()-
tableView!.registerNib(CustomTableViewCell.nib(), forCellReuseIdentifier: CustomTableViewCell.identifier())
//then
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Configure the cell...
let cell = tableView.dequeueReusableCellWithIdentifier(CustomTableViewCell.identifier(), forIndexPath: indexPath) as! CustomTableViewCell
cell.setCellIndexLabel(indexPath.row)
return cell
}
And you will get this:
Is this what you are looking for?
Forgive me for providing swift code, but it's not much different that objective-c!

How to load custom cell ( xib) in UICollectionView cell using swift

I have created a small sample project using Swift. I have created an "MyCustomView" as xib which contains label, button and imageView as shown in below code:
import UIKit
#IBDesignable class MyCustomView: UIView {
#IBOutlet weak var lblName: UILabel!
#IBOutlet weak var btnClick: UIButton!
#IBOutlet weak var myImageView: UIImageView!
var view:UIView!
#IBInspectable
var mytitleLabelText: String? {
get {
return lblName.text
}
set(mytitleLabelText) {
lblName.text = mytitleLabelText
}
}
#IBInspectable
var myCustomImage:UIImage? {
get {
return myImageView.image
}
set(myCustomImage) {
myImageView.image = myCustomImage
}
}
override init(frame : CGRect)
{
super.init(frame: frame)
xibSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
xibSetup()
}
func xibSetup()
{
view = loadViewFromNib()
view.frame = self.bounds
// not sure about this ?
view.autoresizingMask = [UIViewAutoresizing.FlexibleWidth, UIViewAutoresizing.FlexibleHeight]
addSubview(view)
}
func loadViewFromNib() -> UIView {
let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: "MyCustomView", bundle: bundle)
let view = nib.instantiateWithOwner(self, options: nil)[0] as! UIView
return view
}
}
Attached the image of xib for the reference.
In StoryBoard -> ViewController added UIViewCollection which as shown in the below image. In this viewcollection, I need that orange color cell to contain my custom xib to be loaded at runtime.
How do I achieve this?
New Modified code as suggested by Sandeep
// 1
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.register(UINib(nibName: "MyCustomView", bundle: nil), forCellWithReuseIdentifier: "myCell")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 7
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell : MyCustomView = collectionView.dequeueReusableCellWithReuseIdentifier("your_reusable_identifier", forIndexPath: indexPath) as! MyCustomView
cell.lblName.text = "MyNewName"
return cell
}
}
// 2
import UIKit
#IBDesignable class MyCustomView: UICollectionViewCell {
#IBOutlet weak var lblName: UILabel!
#IBOutlet weak var btnClick: UIButton!
#IBOutlet weak var myImageView: UIImageView!
var view:UIView!
#IBInspectable
var mytitleLabelText: String? {
get {
return lblName.text
}
set(mytitleLabelText) {
lblName.text = mytitleLabelText
}
}
#IBInspectable
var myCustomImage:UIImage? {
get {
return myImageView.image
}
set(myCustomImage) {
myImageView.image = myCustomImage
}
}
}
Here is what you can do,
Change your MyCustomView class to be a subclass of UICollectionViewCell and not UIView.
Remove override init(frame : CGRect),required init?(coder aDecoder: NSCoder),func xibSetup(),func loadViewFromNib() -> UIView from MyCustomView
I seriously could not understand how are you using your setter and getter for mytitleLabelText and myCustomImage. If its of no use get rid of it as well.
Finally you will be left with just IBOutlets in MyCustomView.
For better coding practice change the name from MyCustomView to MyCustomCell (optional)
Go to your xib, select the xib and set its class as MyCustomView.
In the same screen change file owner to yourView controller hosting collectionView
In ViewDidLoad of your viewController register your nib.
self.collectionView.registerNib(UINib(nibName: "your_xib_name", bundle: nil), forCellWithReuseIdentifier: "your_reusable_identifier")
In cellForItemAtIndexPath,
let cell : MyCustomView = collectionView.dequeueReusableCellWithReuseIdentifier("your_reusable_identifier", forIndexPath: indexPath) as! MyCustomView
cell.lblName.text = "bla bla" //access your Cell's IBOutlets
return cell
Finally in order to control the size of cell either override the delegate of collectionView or simply go to your collectionView select the collectionCell in it and drag it to match your dimension :) Thats it :)
Happy coding. Search tutorials for better understanding. I can't explain all delegates as I'll end up writing a blog here.
For Swift 4.0
in viewDidLoad:
//custom collectionViewCell
mainCollectionView.register(UINib(nibName: "your_customcell_name", bundle: nil), forCellWithReuseIdentifier: "your_customcell_identifier")
in cellForItemAt indexPath:
let cell : <your_customcell_name> = mainCollectionView.dequeueReusableCell(withReuseIdentifier: "your_customcell_identifier", for: indexPath) as! <your_customcell_name>
And dont forget to set identifier for your custom cell in xib section.
One line approach if you have to register multiple cells.
extension UICollectionViewCell {
static func register(for collectionView: UICollectionView) {
let cellName = String(describing: self)
let cellIdentifier = cellName + "Identifier"
let cellNib = UINib(nibName: String(describing: self), bundle: nil)
collectionView.register(cellNib, forCellWithReuseIdentifier: cellIdentifier)
}
}
Steps on how to use
Name your Cell identifier as "YourcellName" + "Identifier" eg:
CustomCellIdentifier if your cell name is CustomCell.
CustomCell.register(for: collectionView)
For Swift 4.2
in viewDidLoad
self.collectionView.register(UINib(nibName: "your_xib_name", bundle: nil), forCellWithReuseIdentifier: "your_reusable_identifier")
in cellForItemAt indexPath:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "your_reusable_identifier", for: indexPath) as! MyCustomView
And of course as #Raj Aryan said:
don't forget to set identifier for your custom cell in xib section.

SWIFT TableView not responding

I have a problem, i have a controller, and in viewdidload, i try to load a subview created from a xib file.
My "custom" subview is well added to my first controller but the tableview isn't responding... i mean it doesn't scroll, and when i click it, nothing happens... (i haven't implemented yet the method to trigger an action when a cell is clicked, but the celle isn't highlighted when clicked).
Here the code :
class FirstViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let v1 = MyViewTest(frame: CGRectZero)
v1.tableView.dataSource = self
v1.tableView.delegate = self
self.view.addSubview(v1)
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
//datasource method returning the what cell contains
let cell: UITableViewCell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "MyTestCell")
//reusing the previous scrolled cells from table view(if any)
//cell.textLabel?.text = array[indexPath.row]
cell.textLabel?.text = "test"
//passing each elements of the array to cell
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//datasource method returning no. of rows
return 14
}
}
here the code in MyViewTest
class MyViewTest: UIView {
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var tableView: UITableView!
var view:UIView!
let nibName:String = "MyViewTest"
override init(frame: CGRect) {
super.init(frame:frame)
setup()
}
required init(coder aDecoder: NSCoder) {
//fatalError("init(coder:) has not been implemented")
super.init(coder: aDecoder)
setup()
}
func setup() {
view = loadViewFromNib()
let screenSize: CGRect = UIScreen.mainScreen().bounds
let sWidth = screenSize.width
let sHeight = screenSize.height
let xPos = sWidth/2-(view.bounds.width/2)
let yPos = sHeight/2-(view.bounds.height/2)
view.frame = CGRectMake(xPos, yPos, view.bounds.width, view.bounds.height)
addSubview(view)
}
func loadViewFromNib () -> UIView {
let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: nibName, bundle: bundle)
let mview = nib.instantiateWithOwner(self, options: nil)[0] as! UIView
return mview
}
}
in the xib file, i only have a view with a label and my tableview in. The xib file is associated with its class (in identifier).
If you know why my table view isn't working it would be great thx !
You seem to set the frame of the MyTestView to CGRectZero.
You then add your table as a subview of this view with the frame size set up. As MyTestView has 0 width and height and the default for a view is to not clip subviews I imagine you can see the table but not click it.
Try setting the frame of your MyTestView to the screen size?
The problem happens because I instantiate my MyViewTest with CGRectZero. If i use a CGRectMake for example with actual width and height it works great.
See rory mckinnel post just above for more details :-)

Resources