Custom UITableViewCell changes width randomly (swift) - swift

I'm making a chess app for swift with a chat function. I upload my messages to firebase firestore and then, download them into a [Message] array and display them in a UITableView.
I've been looking around for this on stackoverflow and found that several people are having trouble with sizes of a custom UITableViewCell. I've run into the same problem, but in my case the width of the cell seems to randomly change every time I call .reloadData(). To illustrate :
The first 2 times or so when I type something in my chat it should look like this (the cells in the circle)
But as you can see after a few times this happens :
My code :
Custom cell class
class MessageCustomCell: UITableViewCell {
#IBOutlet weak var dateLabel: UILabel!
#IBOutlet weak var bgView: UIView!
#IBOutlet weak var leftImage: UIImageView!
#IBOutlet weak var messageBubble: UIView!
#IBOutlet weak var messageLabel: UILabel!
#IBOutlet weak var rightImage: UIImageView!
override func awakeFromNib() {
super.awakeFromNib()
messageLabel.numberOfLines = 0
messageBubble.layer.cornerRadius = 5
}
}
TableView delegate methods (TableView = messageView)
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return allMessages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = messageView.dequeueReusableCell(withIdentifier: "MessageCustomCell") as! MessageCustomCell
//cell.width = tableView.frame.width
let msg = allMessages[indexPath.row]
//cell.frame.size.width = messageView.frame.width
//cell.contentView.frame.size.width = messageView.frame.width
let date = msg.time
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "d/M/yy, h:mm"
let realDate = dateFormatter.string(from: date)
print(msg.fromUserName!)
if msg.fromUserName! == currentUserName {
cell.messageBubble.backgroundColor = UIColor(named: "lightSquare")
cell.messageLabel.textColor = .black
cell.rightImage.isHidden = true
} else {
cell.messageBubble.backgroundColor = UIColor(named: "darkSquare")
cell.messageLabel.textColor = .white
cell.leftImage.isHidden = true
}
cell.dateLabel.text = realDate
cell.messageLabel.text = msg.text!
return cell
}
In my MessageCustomcell.xib file I have a few constraints :
I've put the leftImage, messageBubble (UIView), rightImage in a horizontal stackview. The images have only width and height constraints. The label in the messageBubble has constraints to its superview (top,bottom,trailing,leading)
The stackView has constraints to the bgView (UIView), also top bottom trailing and leading.
Same for the bgView to the safe area.
I've tried setting the frame for the custom cell, but that didn't seem to work, so I've commented that out.
Really hope someone can help me out. If you guys need more info then please do let me know :)
Thanks all.
EDIT :
Here are my constraints for the custom cell xib file.

Alright thanks to #SPatel, who pointed out to make 2 separate .xib files for the receiver chat bubble and the sender chat bubble. This actually worked for my situation and was the right way to go in the first place, instead of manipulating one custom cell for both sender and receiver.
I'm thinking the constraint issue had to do with me trying to hide either the right image or the left image, to change the custom cell appearance for both sides. That must have messed up the whole constraints situation for the custom cell.
Anyway, happy man. Moving on!

Related

NSTableView.setNeedsDisplay() not redrawing on attached Formatter changes only

i am using a view based NSTableView with a column that shows dates, and the table cell views use a shared DateFormatter.
let view: NSTableCellView? = tableView.makeView(withIdentifier: column.identifier, owner: self) as! NSTableCellView?
let entry = (logController.arrangedObjects as! [LogEntry])[row]
switch column.identifier {
case columnDateKey:
view?.textField?.formatter = sharedDateFormatter
view?.textField?.objectValue = entry.date
The application has a user preference to choose the date format and previously the code
tableView.setNeedsDisplay(tableView.rect(ofColumn: tableView.column(withIdentifier: columnDateKey)))
would refresh the column with the new date format.
With macOS Mojave this does not happen. Investigation shows that although the drawRect: is called for the underlying TableView there are no calls made to tableView(:viewFor:row:) to obtain the new values for table cell views. Calling tableView.reloadData(forRowIndexes:columnIndexes:) does result in calls to tableView(:viewFor:row:) but the display does not refresh (although it does for tableView.reloadData()).
Any external cause to redraw e.g. selecting a row correctly updates that area alone. The other thing I've seen is that with a long table slowly scrolling up will eventually result in the new format appearing although existing cells do not change when scrolled back to until scrolled a long way past before returning. This would seem to infer that there are cached views that are not considered to have changed when only the configuration of the attached formatter changes (although are when the value of the contents changes)
This behaviour changed with the introduction of Mojave and I am finding it difficult to believe that no-one else has reported it and so am beginning to question my original code. Am I missing something?
The following test code demonstrates the problem, the "View requested" message is not printed for variants of setNeedsDisplay and display is only redrawn for reloadData()
styleButton is tick box to toggle number format and refreshButton is action button to request a redraw
Setting the value to a random value will result in expected redraw behaviour
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
#IBOutlet weak var table: NSTableView!
#IBOutlet weak var styleButton: NSButton!
#IBOutlet weak var refreshButton: NSButton!
#IBOutlet weak var testView: NSView!
let numberFormatter = NumberFormatter()
func applicationWillFinishLaunching(_ notification: Notification) {
numberFormatter.numberStyle = symbolButton.state == NSControl.StateValue.on ? NumberFormatter.Style.decimal : NumberFormatter.Style.none
}
#IBAction func refresh(sender: Any?) {
numberFormatter.numberStyle = styleButton.state == NSControl.StateValue.on ? NumberFormatter.Style.decimal : NumberFormatter.Style.none
table.setNeedsDisplay(table.rect(ofColumn: 0))
// table.needsDisplay = true
// table.reloadData(forRowIndexes: IndexSet(integersIn: 0..<table.numberOfRows), columnIndexes:[0])
// table.reloadData()
}
}
extension AppDelegate: NSTableViewDataSource {
func numberOfRows(in tableView: NSTableView) -> Int {
if tableView == table {
return 40
}
return 0
}
}
extension AppDelegate: NSTableViewDelegate {
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
print("View requested")
guard tableColumn != nil else {
return nil
}
let column = tableColumn!
if tableView == table {
let view: NSTableCellView? = tableView.makeView(withIdentifier: column.identifier, owner: self) as! NSTableCellView?
view?.textField?.formatter = numberFormatter
view?.textField?.objectValue = 123.456
return view
}
return nil
}
}
Incorrectly relying on view.setNeedsDisplay to automatically update subviews. This is not the case (although had appeared to work that way, previously) - refer comment from Willeke above

Text is jumbled in custom cell

I'm trying to read data from CoreData into a custom cell. This is just a test app before I try moving to the real app that I've been working on. The data is there - I can print it to the console and see it. For some reason, even with constraints, all of the data is laid on top of each other in the cell. Can anyone see what's going on with this?
I've created constraints to keep the cells where they should be, but when the data is loaded from my 'show data' button, the data is laid on top of each other.
Here is my custom cell class:
import UIKit
class CustomCellClass: UITableViewCell {
#IBOutlet weak var txtNameLabel: UILabel!
#IBOutlet weak var txtAgeLabel: UILabel!
}
Here is the ShowData class: (partial)
class ShowData: UIViewController, NSFetchedResultsControllerDelegate {
#IBOutlet weak var personTableView: UITableView!
let appDelegate = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var personData = [Person]()
// Read the data
override func viewDidLoad() {
super.viewDidLoad()
personTableView.delegate = self
personTableView.dataSource = self
loadItems()
}
func loadItems() {
let request : NSFetchRequest<Person> = Person.fetchRequest()
do {
personData = try appDelegate.fetch(request)
} catch {
print("couldn't load")
}
}
}
extension ShowData : UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return personData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let person = personData[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "personNameAge", for: indexPath) as! CustomCellClass
cell.txtNameLabel?.text = person.name
cell.txtAgeLabel?.text = String(person.age)
return cell
}
Here is a screenshot of the tableview while running:
Edit:
I just deleted the app from the simulator and tried to rerun - now there isn't any data in the cells.
Just to clarify for other readers, as can be seen in your screenshot, the rows in your table are separated as expected but the different fields in each cell, what one might call the columns, are on top of one another.
You say that you have created constraints to keep the cells where they should be, I'm not sure what you mean by that. What you need is constraints for the fields within each cell – what I call intra-cell constraints. Either you have not added these constraints, or there is a mistake in them which causes all fields to be drawn at the left.
To show you what I mean, let's use the example of a little workout app of mine which has, in each table cell, from left to right, a Perform button, an Edit button, a Name field and a Duration field. The screenshot below shows, in the big yellow box, the intra-cell constraints. If you are using a storyboard, the problem with your app must be in that area. If you are not using a storyboard, the problem must be in the equivalent code (or lack of it).
Just to let everyone know. The issue is resolved. I removed the table view cell from the project, readded, and readded the constraints. Everything is working now. I'm not sure where the problem was, but I noticed I had weird wrapping happening. I moved one of the labels to the other side of the cell and constrained it to the right side, and the other to the left side. When I ran the app, the text appeared to word wrap. I decided to delete the cell and readd and relink my outlets. It worked the first time...

How do I use UIStepper to change a property in a Realm Model Object?

I currently have a ViewController with a TableView inside it called SelectedListItemsViewController. This ViewController's TableView is populated by a custom cell class called SelectedListItemsTableViewCell.
I have an array of Realm Model Objects called selectedListItems, each of which has several properties. The SelectedListItemsTableViewCell populates the TableView with the listItem property of that indexPath.row's object, and each row has a UIStepper with a UILabel next to it that (as of now) shows UIStepper.value for each row. Ideally, the label will reflect the listItemWeight property of each row, and change it when incrementing or decrementing that row.
This is my custom cell:
class SelectedListItemsTableViewCell: UITableViewCell {
#IBOutlet var selectedListItemLabel: UILabel!
#IBOutlet weak var listItemWeightLabel: UILabel!
#IBOutlet weak var stepperControl: UIStepper!
#IBAction func stepperValueChanged(sender: UIStepper) {
listItemWeightLabel.text = Int(sender.value).description
}
}
And in my ViewController's cellForRowAtIndexPath, I've configured the cell like so:
// Configure the cell...
cell.selectedListItemLabel.text = selectedListItems[indexPath.row].listItem
cell.listItemWeightLabel.text = "\(selectedListItems[indexPath.row].listItemWeight)"
Which perfectly loads the listItem property, and the listItemWeight property shows up correctly, but as soon as I increment or decrement on the UIStepper it gets messed up.
How do I properly link my UILabel and UIStepper to the [indexPath.row].listItemWeight?
In the same method that gets called when your stepper updates, update your listItem. However, since this item is stored in your Realm database, you will have to get an instance of your Realm database and write the change to the database.
You can do this by having your TableViewCell hold on to an instance of the listItem. Your new TableViewCell class will look something like this:
class SelectedListItemsTableViewCell: UITableViewCell {
#IBOutlet var selectedListItemLabel: UILabel!
#IBOutlet weak var listItemWeightLabel: UILabel!
#IBOutlet weak var stepperControl: UIStepper!
var listItem: Item?
#IBAction func stepperValueChanged(sender: UIStepper) {
listItemWeightLabel.text = Int(sender.value).description
if let listItem = listItem {
let realm = try! Realm
try! realm.write {
listItem.listItemWeight = Int(sender.value) ?? 0
}
}
}
}
The above answer was helpful in leading me to the actual solution of my issue, but if anyone in the future is curious - I ended up using a closure.
In my custom cell class, I did
#IBAction func stepperValueChanged(sender: UIStepper) {
selectedListItemLabel.text = Int(sender.value).description
tapped?(self)
}
And in my view controller, I did
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! SelectedListItemsTableViewCell
cell.stepperControl.value = selectedListItems[indexPath.row].listItemWeight
// Configure the cell...
cell.tapped = { (selectedCell) -> Void in
selectedListItems[indexPath.row].listItemWeight = cell.stepperControl.value
}
Which allowed me to access each cell's UIStepper in the view controller file.
It helped to read flashadvanced's Option 2 answer in this thread.

UIImageView and UILabel display issues in a UITableViewCell using Swift

I'm having issues displaying some image views and text labels using a custom UITableViewCell.
This is what is happening:
A shot of the storyboard:
Some of the issues is that the UILabel text does not get replace and it's ghosting. Also, for the 2nd row, there should only be 2 UIImageViews but there's 3, because there's text for the middle that shouldn't have been shown.
AboutCustomCell.swift:
class AboutCustomCell: UITableViewCell
{
#IBOutlet var threePicRow_leftImageView: UIImageView!
#IBOutlet var threePicRow_leftNameLabel: UILabel!
#IBOutlet var threePicRow_leftPositionLabel: UILabel!
...
#IBOutlet var twoPicRow_leftImageView: UIImageView!
#IBOutlet var twoPicRow_leftNameLabel: UILabel!
#IBOutlet var twoPicRow_leftPositionLabel: UILabel!
...
}
cellForRowAtIndexPath:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell3Pic = tableView.dequeueReusableCellWithIdentifier("3PictureRowCell", forIndexPath: indexPath) as! AboutCustomCell
let cell2Pic = tableView.dequeueReusableCellWithIdentifier("2PictureRowCell", forIndexPath: indexPath) as! AboutCustomCell
switch (indexPath.row)
{
case 0:
cell3Pic.threePicRow_leftNameLabel.text = "Name"
cell3Pic.threePicRow_leftPositionLabel.text = "Position"
cell3Pic.threePicRow_leftImageView.image = UIImage(named: "icon_about")
cell3Pic.threePicRow_middleNameLabel.text = "Name"
cell3Pic.threePicRow_middlePositionLabel.text = "Position"
cell3Pic.threePicRow_middleImageView.image = UIImage(named: "icon_about")
cell3Pic.threePicRow_rightNameLabel.text = "Name"
cell3Pic.threePicRow_rightPositionLabel.text = "Position"
cell3Pic.threePicRow_rightImageView.image = UIImage(named: "icon_about")
return cell3Pic
case 1:
cell2Pic.twoPicRow_leftNameLabel.text = "Name"
cell2Pic.twoPicRow_leftPositionLabel.text = "Position"
cell2Pic.twoPicRow_leftImageView.image = UIImage(named: "icon_about")
cell2Pic.twoPicRow_rightNameLabel.text = "Name"
cell2Pic.twoPicRow_rightPositionLabel.text = "Position"
cell2Pic.twoPicRow_rightImageView.image = UIImage(named: "icon_about")
return cell2Pic
default:
break;
}
// Will never reach here
return cell3Pic
}
Is there something I'm not doing to the custom cell that would cause this? Thank you.
Here are your options:
Most close to your own design, make sure you hide and show the images and labels that should not be shown explicitly in cellForRowAtIndexPath, by setting their hidden property.
Better: create two different cell classes, one with 2 and one with three items per row. Dequeue only the cells you need and return those in cellForRowAtIndexPath.
Best: think about exploring UICollectionView instead.
First of all, I can't reproduce this behavior since I don't have any Apple hardware right here.
But I suspect the problem comes from the fact that table cells are reused or that always some outlets aren't properly connected but get nevertheless instantiated.
Three possible solutions:
The minimal/dirty one: Set the text of the unused outlet labels to the empty strings.
The elaborate one: Define two different cell classes for the two and three picture rows.
The completely different one: If you actually don't have dynamic content inside your table view don't use a table view, instead use a hand taylored layout.

iOS8/Swift: UICollectionViewCell height calculation

deriving from other posts about this topic i tried the following approach - but somehow "foo" as well as "bar" seem to be nil
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewFlowLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
var foo = collectionView.cellForItemAtIndexPath(indexPath) as? messageCell
var bar = foo?.messageText.frame.height
return CGSizeMake(self.view.frame.width-20, bar!+20)
}
the class for the custom cell:
class messageCell: UICollectionViewCell,UIPopoverPresentationControllerDelegate,UITextViewDelegate{
#IBOutlet weak var authorLabel: UILabel!
#IBOutlet weak var timestampLabel: UILabel!
#IBOutlet weak var messageText: UITextView!
func setCell(authorLabelText:String, messageText:String,timestampLabelText:String) {
self.authorLabel?.text = authorLabelText
self.timestampLabel?.text = timestampLabelText
self.messageText?.text = messageText
self.messageText.delegate = self
self.messageText.sizeToFit() //works, i can see different heights when giving a custom color background
println(self.messageText.frame.height)
}
}
Am I supposed to set the height in my custom cell class? That would make no sense as it isnt rendered yet, wouldn't it?
€dit:
A Screenshot from stopping at "foo":
bar is nil because foo is nil - that's what the optional chaining "?" is doing in that expression. If collectionView.cellForItemAtIndexPath(indexPath) as? MessageCell† is nil, it would imply either:
There is no cell at that index path, or
The cell at indexPath is not an instance of MessageCell
I would suggest it's more likely to be the latter, as those delegate messages shouldn't really call with an invalid index path. Where you register your cell class (either in code, or in the storyboard), are you sure you're registering them as MessageCell cells?
† Types should begin with an uppercase character