I ran into an issue that I'm not sure how to go about solving. I have a Cell object where I create some IBOutlets that I want to display inside my UITableView to my screen like so:
class EventCell: UITableViewCell, CellDelegate{
#IBOutlet var eventName: UILabel!
#IBOutlet var eventLocation: UILabel!
#IBOutlet var attendeesImages: [UIImageView]!
}
I also have another function where I try to set the cell contents of that cell like so:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
//Dequeue a "reusable" cell
let cell = tableView.dequeueReusableCellWithIdentifier(eventCellIdentifier) as! EventCell
setCellContents(cell, indexPath: indexPath)
return cell
}
//Set contents of Event Cell.. self.events is a global array
//which have information that EventCell objects need to display
func setCellContents(cell:EventCell, indexPath: NSIndexPath!){
let item = self.events[indexPath.section]
var count = 0
cell.eventName.text = item.eventName()
cell.eventLocation.text = item.eventLocation()
//Attempt to set the cell.attendeesImage values
for value in item.attendeesImages() {
cell.attendeesImages[count].image = value
cell.attendeesImages[count].clipsToBounds = true
cell.attendeesImages[count].layer.cornerRadius = cell.attendeesImage[count].frame.size.width / 2
count++
}
}
The issue is, when I try to set the values of cell.attendeesImages, I run into an issue saying fatal error: Array index out of range. The issue is because I am accessing an index that doesn't exist in cell.attendeesImages. Is there any way to allocate or set the size of cell.attendeesImages before I assign its contents? Or is there simply a better way to assign the images within the cell.attendeesImages? I also tried to set cell.attendeesImages as an array of UIImage and setting cell.attendeesImages = item.attendeesImages but if it is as an UIImage, I can't seem to display the images onto the screen whereas the UIImageView will allow me to do so. Any help would be appreciated. Thanks!
Replace
for value in item.attendeesImages() {
cell.attendeesImages[count].image = value
cell.attendeesImages[count].clipsToBounds = true
cell.attendeesImages[count].layer.cornerRadius = cell.attendeesImage[count].frame.size.width / 2
count++
}
With
for (count, value) in enumerate(item.attendeesImages()) {
var workingImage : UIImageView! = nil
if count >= cell.attendeesImages.count {
workingImage = UIImageView(image : value)
cell.addSubview(workingImage)
cell.attendeesImages.append(workingImage)
//TODO: Layout image based on count
} else {
workingImage = cell.attendeesImages[count]
}
workingImage.clipsToBounds = true
workingImage.layer.cornerRadius = workingImage.frame.size.width / 2
}
//Loop through remaining images from cell-reuse and hide them if they exist
for var index = item.attendeesImages().count; index < cell.attendeesImages.count; ++index {
cell.attendeesImages[index].image = nil
}
Related
I have a custom view(tagListView) inside a custom tableview cell.
When I call addTag to cell.tagListView inside "cellForRowAt", it adds a tag for every cell.
How do I add tag only for that cell? I tried to keep a count so I only add tags to those which don't have tags. But it appears that this count is the same for all cells? I know this has something to do with reusable cell.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = topicListTableView.dequeueReusableCell(withIdentifier: "TopicListTableCell", for: indexPath)
if let myCell = cell as? TopicListTableCell {
if let model=topicListModel{
let t=model.topics[(indexPath.row)]
if let subject=t.subject{
let (tags,title)=util().getTag(subject: subject)
myCell.subject.text=title
if(myCell.tagCount==0){
myCell.tagList.addTags(tags)
myCell.tagCount+=1
}
}
myCell.content.text=t.content
myCell.authorTime.text=t.author
if let replynum=t.replies{
myCell.commentNum.text=String(replynum)
}
if let upvoteNum=t.score{
myCell.upVote.text=String(upvoteNum)
}
if indexPath.row%2==1{
myCell.background.backgroundColor=util().lightyellow
}else{
myCell.background.backgroundColor=util().darkeryellow
}
}
}
return cell
}
code for cell:
class TopicListTableCell: UITableViewCell{
#IBOutlet weak var content: UILabel!
#IBOutlet weak var upVote: UILabel!
#IBOutlet weak var commentNum: UILabel!
#IBOutlet weak var subject: UILabel!
#IBOutlet weak var authorTime: UILabel!
#IBOutlet weak var background: UIView!
#IBOutlet weak var tagList: TagListView!
var tagCount = 0
}
As dfd already stated, your approach isn't the right one because of the way UITableViews are working in iOS.
You should have one dataSource for your tableView (some array or dictionary) which contains all the information needed to present the cells the way you want.
So I would make a struct which contains all information I need to fill one cell and then make an array of these structs to fill all cells with the information they need.
Something like this:
// In your model:
struct TopicItem {
let topic: Topic
let tags: [Tag]
let title: String
}
var topicItems = [TopicItem]()
// This is function is only a draft, so you get a better idea what i mean
func updateTopicItems() {
topicItems.removeAll()
for topic in topics {
let tags: [Tag]
let title: String
if let subject = topic.subject {
// I would refactor this function, it is called getTag() but it returns tags and title. Kinda confusing
(tags,title) = util().getTag(subject: subject)
} else {
tags = [Tag]()
title = ""
}
topicItems.append(TopicItem(topic: topic, tags: tags, title: title))
}
}
I also have added some refactoring and comments which will hopefully help you to keep your code clean and maintainable in the future :)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// cell can be force unwrapped, it will never be something different than a TopicListTableCell, unless you change your code
// and even then you want a crash, so you find the error fast (crash will only occur during development)
let cell = topicListTableView.dequeueReusableCell(withIdentifier: "TopicListTableCell", for: indexPath) as! TopicListTableCell
guard let model = topicListModel else { return cell } // If there is no model, there is nothing to do
let topicItem = model.topicItems[(indexPath.row)]
let t = topicItem.topic
cell.subject.text = topicItem.title
// You should replace the addTags() function with a setTags() function, otherwise you will get more and more tags every time you present the cell
cell.tagList.addTags(topicItem.tags)
cell.content.text = t.content
cell.authorTime.text = t.author
if let replynum = t.replies {
cell.commentNum.text = String(replynum)
} else {
// Because cells are getting reused, you need to always set all fields, otherwise you get cells with content from other cells
cell.commentNum.text = ""
}
if let upvoteNum = t.score {
cell.upVote.text = String(upvoteNum)
} else {
// Again always set all fields
cell.upVote.text = ""
}
if indexPath.row %2 == 1 {
cell.background.backgroundColor=util().lightyellow
} else {
cell.background.backgroundColor=util().darkeryellow
}
return cell
}
I have a disconcerting issue in that I have a UITableViewCell that does not update the displayed value of its underlying data. To the code:
class ReviewInspectionViewController: UIViewController {
private lazy var locationsDataSource: ReviewInspectionDataSource = ReviewInspectionDataSource(tableView: tableView, delegate: self)
override func viewWillAppear(_ animated: Bool) {
.. retrieve data from Realm
.. process data and place in data object defined as var data : [Any] = []
locationsDataSource.data.append((location.title,data))
}
}
class ReviewInspectionDataSource: NSObject, UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let location = data[indexPath.section]
let item = location.content[indexPath.row]
if let item = item as? String {
let cell = tableView.dequeueReusableCell(for: indexPath, cellType: ReviewChecklistItemCell.self)
cell.descriptionLabel.text = item
return cell
}
....
}
}
Works fine the first time and the correct string is shown on the screen. I tab to a different view (the underlying UIViewController is in a UITabViewController), make a change and then tab back, I can confirm that the changed data is being set correctly in this line:
cell.descriptionLabel.text = item
I can even print out the value of cell.description.text by adding a line like this:
cell.descriptionLabel.text = item
print("Cell value", cell.descriptionLabel.text)
and it prints out the changed value BUT the screen shows the old value. The UITableViewCell itself is extremely simple:
class ReviewChecklistItemCell: UITableViewCell, NibReusable {
#IBOutlet weak var descriptionLabel: UILabel!
}
The datasource class is loaded from the UIViewController.viewWillAppear method holding onto the UITableView. I have never seen this happen before, thoughts on what the issue is?
It sounds like you are updating the cell description label instead of updating the actual source of the data.
So instead of
let cell = tableView.dequeueReusableCell(for: indexPath, cellType: ReviewChecklistItemCell.self)
cell.descriptionLabel.text = item
I would update your data source at that indexPath
dataSource[indexPath.row] = item
tableView.reloadData()
The title might be a little hard to understand, but this situation might help you with it.
I'm writing a multiple image picker. Say the limit is 3 pictures, after the user has selected 3, all other images will have alpha = 0.3 to indicate that this image is not selectable. (Scroll all the way down to see a demo)
First of all, this is the code I have:
PickerPhotoCell (a custom collection view cell):
class PickerPhotoCell: UICollectionViewCell {
#IBOutlet weak var imageView: UIImageView!
var selectable: Bool {
didSet {
self.alpha = selectable ? 1 : 0.3
}
}
}
PhotoPickerViewController:
class PhotoPickerViewController: UICollectionViewController {
...
var photos: [PHAsset]() // Holds all photo assets
var selected: [PHAsset]() // Holds all selected photos
var limit: Int = 3
override func viewDidLoad() {
super.viewDidLoad()
// Suppose I have a func that grabs all photos from photo library
photos = grabAllPhotos()
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell ...
let asset = photos[indexPath.row]
...
// An image is selectable if:
// 1. It's already selected, then user can deselect it, or
// 2. Number of selected images are < limit
cell.selectable = cell.isSelected || selected.count < limit
return cell
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as! PickerPhotoCell
if cell.isSelected {
// Remove the corresponding PHAsset in 'selected' array
} else {
// Append the corresponding PhAsset to 'selected' array
}
// Since an image is selected/deselected, I need to update
// which images are selectable/unselectable now
for visibleCell in collectionView.visibleCells {
let visiblePhoto = visibleCell as! PickerPhotoCell
visiblePhoto.selectable = visiblePhoto.isSelected || selected.count < limit
}
}
}
This works almost perfectly, except for one thing, look at the GIF:
The problem is
After I've selected 3 photos, all other visible photos have alpha = 0.3, but when I scroll down a little more, there are some photos that still have alpha = 1. I know why this is happening - Because they were off-screen, calling collectionView.visibleCells wouldn't affect them & unlike other non-existing cells, they did exist even though they were off-screen. So I wonder how I could access them and therefore make them unselectable?
The problem is that you are trying to store your state in the cell itself, by doing this: if cell.isSelected.... There are no off screen cells in the collection view, it reuses cells all the time, and you should actually reset cell's state in prepareForReuse method. Which means you need to store your data outside of the UICollectionViewCell.
What you can do is store selected IndexPath in your view controller's property, and use that data to mark your cells selected or not.
pseudocode:
class MyViewController {
var selectedIndexes = [IndexPath]()
func cellForItem(indexPath) {
cell.isSelected = selectedIndexes.contains(indexPath)
}
func didSelectCell(indexPath) {
if selectedIndexes.contains(indexPath) {
selectedIndexes.remove(indexPath)
} else if selectedIndexes.count < limiit {
selectedIndexes.append(indexPath)
}
}
}
I'm attempting to pass an array of data from the view controller to the collection view cells. My collectionview is currently in a tableview. I have tried using delegation/protocols and creating arrays in the class and have not been able to successfully pass the data to my collectionview.
My code is a follows:
View Controller:
var ageUnder10: [MissingPerson] = []
var age10Plus: [MissingPerson] = []
var age15Plus: [MissingPerson] = []
if let ageRange = ageRange {
switch ageRange {
case .ageUnder10:
let ageUnder10Array = MissingPerson()
ageUnder10Array.title = self.missingPerson.title
ageUnder10Array.desc = self.missingPerson.desc
ageUnder10Array.url = self.missingPerson.url
self.ageUnder10.append(ageUnder10Array)
case .age10Plus:
let age10PlusArray = MissingPerson()
age10PlusArray.title = self.missingPerson.title
age10PlusArray.desc = self.missingPerson.desc
age10PlusArray.url = self.missingPerson.url
self.age10Plus.append(age10PlusArray)
case .age15Plus:
let age15PlusArray = MissingPerson()
age15PlusArray.title = self.missingPerson.title
age15PlusArray.desc = self.missingPerson.desc
age15PlusArray.url = self.missingPerson.url
self.age15Plus.append(age15PlusArray)
}
} else {
print("No valid age found")
}
Tableview Cell:
class TableViewCell: UITableViewCell {
var ageUnder10 = [MissingPerson]()
var age10Plus = [MissingPerson]()
var age15Plus = [MissingPerson]()
}
These values are being populated from an XML url
The categories are being created via scanner, scanning the values of a item in the xml (to create ageRange)
I have titleforheader and header names populated from a separate array in the view controller class
I figured it out, I needed to use a struct to pass the data. Also, create an instance of the array in the tableview class and write a function to fill the collectionView cell.
Example:
CustomTableViewCell:
customArray: [CustomArray]()
func configureCollectionCell(with array: [CustomArray]) {
self.customArray = customArray
}
ViewController Class:
var customArray = [CustomArray]()
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as? CustomTableViewCell {
cell.configureCollectionCell(with: customArray)
return cell
}
I'm making a very simple app where the user enters the number of people in the first Screen.
In the second screen it generates a number of UITableViewCell based on the number the user entered in the first screen. The UITableViewCell have a UITextField in them and I'm trying to store the data entered in those fields in an array once the user clicks to go to the third screen.
How can I do that? Thanks in advance!
Edit: I'm using the storyboard.
Here is what the code that calls for the custom UITableViewCell looks like for my UIViewController:
func tableView(tableView:UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
var cell: EditingCell = tableView.dequeueReusableCellWithIdentifier("Cell") as EditingCell
if indexPath.row % 2 == 0{
cell.backgroundColor = UIColor.purpleColor()
} else {
cell.backgroundColor = UIColor.orangeColor()
}
let person = arrayOfPeople[indexPath.row]
cell.setCell(person.name)
return cell
}
Here is what the code for the UITableViewCell looks like:
class EditingCell: UITableViewCell{
#IBOutlet weak var nameInput: UITextField!
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
}
func setCell(name:String){
self.nameInput.placeholder = name
}
}
There is a problem with your approach if the number of rows in your table exceeds the number that can fit on screen. In that case, the cells that scroll off-screen will be re-used, and the contents of the nameInput textField will be lost. If you can be sure that this will never happen, use the following code (in the method that handles button taps) to compose your array:
var arrayOfNames : [String] = [String]()
for var i = 0; i<self.arrayOfPeople.count; i++ {
let indexPath = NSIndexPath(forRow:i, inSection:0)
let cell : EditingCell? = self.tableView.cellForRowAtIndexPath(indexPath) as EditingCell?
if let item = cell?.nameInput.text {
arrayOfNames.append(item)
}
}
println("\(arrayOfNames)")
Alternatively....
However, if it is possible that cells will scroll off-screen, I suggest a different solution. Set the delegate for the nameInput text fields, and then use the delegate methods to grab the names as they are entered.
First, add variables to your view controller, to hold the array and the row number of the text field currently being edited.
var arrayOfNames : [String] = [String]()
var rowBeingEdited : Int? = nil
Then, in your cellForRowAtIndexPath method, add:
cell.nameInput.text = "" // just in case cells are re-used, this clears the old value
cell.nameInput.tag = indexPath.row
cell.nameInput.delegate = self
Then add two new functions, to catch when the text fields begin/end editing:
func textFieldDidEndEditing(textField: UITextField) {
let row = textField.tag
if row >= arrayOfNames.count {
for var addRow = arrayOfNames.count; addRow <= row; addRow++ {
arrayOfNames.append("") // this adds blank rows in case the user skips rows
}
}
arrayOfNames[row] = textField.text
rowBeingEdited = nil
}
func textFieldDidBeginEditing(textField: UITextField) {
rowBeingEdited = textField.tag
}
When the user taps the button, they might still be editing one of the names. To cater for this, add the following to the method that handles the button taps:
if let row = rowBeingEdited {
let indexPath = NSIndexPath(forRow:row, inSection:0)
let cell : EditingTableViewCell? = self.tableView.cellForRowAtIndexPath(indexPath) as EditingTableViewCell?
cell?.nameTextField.resignFirstResponder()
}
This forces the textField to complete editing, and hence trigger the didEndEditing method, thereby saving the text to the array.
Here for new swift versions of answer
var arrayOfNames : [String] = [String]()
var i = 0
while i < taskArrForRead.count {
let indexPath = IndexPath(row: i, section: 0)
let cell : taslakDuzenlemeCell? = self.tableView.cellForRow(at: indexPath) as! taslakDuzenlemeCell?
if let item = cell?.taslakTextField.text {
arrayOfNames.append(item)
}
i = i + 1
}
print("\(arrayOfNames)")