My constraints are broke when I pop VC. I have table view, when I firstly push table view, constraints are work perfectly, but after poping and pushing again they are broke. First image are working good, but in second they are broke Maybe problem is in didSet, after breaking constraints xcode says to remove width, but ui will be bad
This is my cell code
var isIncoming: Bool! {
didSet {
bubbleBackgroundView.backgroundColor = isIncoming ? Constants.Color.inComingMessages : Constants.Color.outComingMessages
messageLabel.textColor = isIncoming ? .black : .white
if isIncoming {
bubbleBackgroundView.snp.makeConstraints { (make) in
make.leading.equalTo(16)
make.width.equalTo(180)
}
messageLabel.snp.makeConstraints { (make) in
make.leading.equalTo(16)
}
addSubview(inComingImgTail)
inComingImgTail.snp.makeConstraints { (make) in
make.trailing.equalTo(bubbleBackgroundView.snp.leading).offset(12)
make.top.equalTo(bubbleBackgroundView.snp.top).offset(1)
make.width.equalTo(15)
make.height.equalTo(36)
}
}
else if !isIncoming{
bubbleBackgroundView.snp.makeConstraints { (make) in
make.trailing.equalTo(self).offset(-16)
make.width.equalTo(180)
}
messageLabel.snp.makeConstraints { (make) in
make.leading.equalTo(16)
make.trailing.equalTo(-16)
}
addSubview(OutComingImgTail)
OutComingImgTail.snp.makeConstraints { (make) in
make.trailing.equalTo(bubbleBackgroundView.snp.trailing).offset(4)
make.top.equalTo(bubbleBackgroundView.snp.top).offset(1)
make.width.equalTo(15)
make.height.equalTo(36)
}
}
}
}
This is my dataSource and delegate code
extension ChatVC: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return ChatVC.messagesList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: MessagesCell.reuseIdentifier, for: indexPath) as! MessagesCell
let messages = ChatVC.messagesList[indexPath.row]
let sorted = ChatVC.messagesList.sorted { (first, second) -> Bool in
return first.pk < second.pk
}
cell.messagesLists = messages
cell.messageLabel.text = sorted[indexPath.row].text
if sorted[indexPath.row].from_user == userID {
cell.isIncoming = false
}
else if sorted[indexPath.row].from_user != userID {
cell.isIncoming = true
}
return cell
}
}
The cells are being reused every time, and to switch modes from .sent to .received, you have to deactivate previous constraints. Add two arrays of constraints for both states, and deactivate the previous array and activate the new one.
In your case you just append constraints, but don't remove them
var receivedConstraints = [NSLayoutConstraint]()
var sentConstraints = [NSLayoutConstraint]()
...
final func changeCellMode(to mode: SNKMessage.Mode, didChangeMessageMode: (Bool) -> ()) {
if let _previousMode = previousMessageMode, _previousMode == mode {
didChangeMessageMode(false)
messageBackground.setNeedsDisplay()
return
}
didChangeMessageMode(true)
if mode == .outgoing {
NSLayoutConstraint.deactivate(receivedConstraints)
NSLayoutConstraint.activate(sentConstraints)
} else {
NSLayoutConstraint.deactivate(sentConstraints)
NSLayoutConstraint.activate(receivedConstraints)
}
messageBackground.messageMode = mode
previousMessageMode = mode
setNeedsLayout()
}
First of all, i recommend you to use collectionview for this kind of a view. In this part of source, you should not set constraints every time. By the way please watch the link below. :)
https://youtu.be/kR9cf_K_9Tk
Related
Issue: When I scroll the tableView, my configureCell() method appends too many views to the stackView inside the cell.
When the cell is first displayed, and I press the UIButton, the stack is unhidden and first shows the right amount of views, after scrolling, the amount is duplicated.
prepareForReuse() is empty right now. I want to keep the stackView unHidden after scrolling.
I set the heightAnchor for the UIView as it is programmatic layout.
Expected Outcome: User taps on the button, the cell stackView is unhidden and the cell expands to chow the uiviews related to the cell.
When I call it in the cellForRowAt, nothing happens. Because im not sure how to modify the method for IndexPath.row.
protocol DataDelegate: AnyObject {
func displayDataFor(_ cell: TableViewCell)
}
class TableViewCell: UITableViewCell {
#IBOutlet weak var stackView: UIStackView! {
didSet {
stackView.isHidden = true
}
}
#IBOutlet button: UIButton!
var model = Model()
var detailBool: Bool = false
#IBAction func action(_ sender: Any) {
self.claimsDelegate?.displayClaimsFor(self)
detailBool.toggle()
}
func configureCellFrom(model: Model) {
if let addedData = model.addedData {
if addedData.count > 1 {
for data in addedData {
let dataView = DataView()
dataView.heightAnchor.constraint(equalToConstant: 65).isActive = true
dataView.dataNumber.text = data.authDataNumber
self.stackView.addArrangedSubview(dataView)
}
}
}
}
}
How would I call this in cellForRowAt, so its only created the correct amount of uiviews and not constantly adding?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCell.identifier, for: indexPath) as? TableViewCell else {
return UITableViewCell()
}
cell.dataDelegate = self
cell.configureCellFrom(model: model[indexPath.row])
//if let addedData = model.addedData {
//if addedData.count > 1 {
//for data in addedData {
//let dataView = DataView()
//dataView.heightAnchor.constraint(equalToConstant: 65).isActive = true
//dataView.dataNumber.text = data.authDataNumber
//self.stackView.addArrangedSubview(dataView)
// }}}
cell.layoutIfNeeded()
cell.selectionStyle = .none
return cell
}
extension ViewController: DataDelegate {
func displayDataFor(_ cell: TableViewCell) {
if tableView.indexPath(for: cell) != nil {
switch cell.detailBool {
case true:
UIView.performWithoutAnimation {
tableView.beginUpdates()
cell.detailArrow.transform = CGAffineTransform(rotationAngle:.pi)
cell.stackView.isHidden = false
tableView.endUpdates()
}
case false:
UIView.performWithoutAnimation {
tableView.beginUpdates()
cell.stackView.isHidden = true
cell.detailArrow.transform = CGAffineTransform.identity
tableView.endUpdates()
}
}
}
}
}
If I understand correctly, you would like to create an expandable TableView? If yes you can do it a lot of different ways, but you have to change your approach totally. Please refer LBTA approach:
LBTA video
My favourite the Struct approach, where you create a struct and you can save the complication with the 2D array:
Struct stackoverflow link
I am trying to update a tableView to UITableViewDiffableDataSource but I am having trouble with being able to delete/move the rows. With some help from a previous question, I have subclassed the dataSource and added the tableview overrides there. The rows will edit but when I leave and come back to the view they go back to the way they were as my data models in my VC are not updating with the edits. Can anyone help with this?
extension NSDiffableDataSourceSnapshot {
mutating func deleteItemsAndSections(_ items : [ItemIdentifierType]) {
self.deleteItems(items)
let emptySection = self.sectionIdentifiers.filter {
self.numberOfItems(inSection: $0) == 0
}
self.deleteSections(emptySection)
}
}
fileprivate class ListDrillDownDataSource: UITableViewDiffableDataSource<String, ListItem> {
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
guard editingStyle == .delete else { return }
// Delete the row from the data source
if let item = self.itemIdentifier(for: indexPath) {
var snapshot = self.snapshot()
snapshot.deleteItemsAndSections([item])
self.apply(snapshot, animatingDifferences: true)
}
}
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return true
}
}
Edit**
I have made some progress by adding a backingStore and I am closer to being able to update an existing cell. However, I ultimately just keep getting stuck in the same loop of errors.
I started by adding a backingStore property who's values are a custom ListItem class. The class is hashable with a UUID() as one of the properties.
class ListDrillDownTableViewController: UITableViewController {
fileprivate var dataSource: ListDrillDownDataSource!
fileprivate var currentSnapshot: ListDrillDownSnaphot?
var list: List = List(name: "Test List", listStyle: .preSetStyle[0], listItems: [], users: [])
var backingStore = [UUID: ListItem]()
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.leftBarButtonItem = self.editButtonItem
self.dataSource = createDataSource()
updateUI(animated: false)
tableView.rowHeight = 65
}
Then I created a method to create the dataSource giving the cell provider the UUID to look up the listItem. I also updated the UI with the initial snapshot populating the UUIDs and the backingStore.
fileprivate func createDataSource() -> ListDrillDownDataSource {
let dataSource = ListDrillDownDataSource(tableView: tableView) { (tableView, indexPath, uuid) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: "ListItemCell", for: indexPath) as! ListItemCell
guard let listItem = self.backingStore[uuid] else { return cell }
cell.titleLabel.text = listItem.title
if let cost = listItem.cost {
cell.costLabel.isHidden = false
cell.costLabel.text = (listItem.costString(cost: cost))
} else {
cell.costLabel.isHidden = true
}
if listItem.note == "" {
cell.noteIcon.isHidden = true
} else {
cell.noteIcon.isHidden = false
}
if listItem.askToReschedule && !listItem.hasRepeatInterval {
cell.repeatIcon.isHidden = false
cell.repeatIcon.image = UIImage(systemName: "plus.bubble")
} else if !listItem.askToReschedule && listItem.hasRepeatInterval {
cell.repeatIcon.isHidden = false
cell.repeatIcon.image = UIImage(systemName: "repeat")
} else {
cell.repeatIcon.isHidden = true
}
return cell
}
self.tableView.dataSource = dataSource
return dataSource
}
func updateUI(animated: Bool = true) {
var snapshot = ListDrillDownSnaphot()
snapshot.appendSections(["main"])
let uuids = self.list.listItems.map { _ in UUID() }
snapshot.appendItems(uuids)
for (uuid, listItem) in zip(uuids, list.listItems) {
self.backingStore[uuid] = listItem
}
self.dataSource.apply(snapshot, animatingDifferences: animated)
}
Finally, when the user taps a cell, makes there edits and taps done the view unwinds back to this controller and I attempt to update the dataSource in the unwind method. As it stands the app will create the first new cell but when a second is attempted it reloads the first row twice and as I keep adding it doubles the existing rows. I'm guessing because I'm appending the whole list over and over, but I can't figure out how to access JUST the appended uuid.
When I tap into an existing cell and edit the info it works, but if I go in again to the row and come out the cell goes back to it's original state. If I keep trying it bounces back and forth between the original state and the updated, almost like it is bouncing back and forth between snapshots?
#IBAction func unwindToListDrillDownTableView(_ unwindSegue: UIStoryboardSegue) {
// Verify the correct segue is being used.
guard unwindSegue.identifier == "DoneAddEditListItemUnwind" else { return }
let sourceViewController = unwindSegue.source as! ListItemDetailTableViewController
// Update the Lists categories with any changes.
self.list.listStyle?.categories = sourceViewController.currentCategories
// Verify a ListItem was returned.
if let listItem = sourceViewController.listItem {
// Check if ListItem is existing.
if let indexOfExistingListItem = list.listItems.firstIndex(of: listItem) {
// If existing, update the ListItem and then the view.
list.listItems[indexOfExistingListItem] = listItem
let uuid = self.dataSource.itemIdentifier(for: IndexPath(row: indexOfExistingListItem, section: 0))
let uuids = self.list.listItems.map { _ in UUID() }
var snapshot = self.dataSource.snapshot()
snapshot.reloadItems([uuid!])
for (uuid, listItem) in zip(uuids, list.listItems) {
self.backingStore[uuid] = listItem
}
self.dataSource.apply(snapshot, animatingDifferences: true)
} else {
// If new, add to listItems collection and update the view.
list.listItems.append(listItem)
if self.backingStore.keys.isEmpty {
updateUI()
} else {
var snapshot = self.dataSource.snapshot()
let uuids = self.list.listItems.map { _ in UUID() }
snapshot.reloadSections(["main"])
snapshot.appendItems(uuids)
for (uuid, listItem) in zip(uuids, list.listItems) {
self.backingStore[uuid] = listItem
}
self.dataSource.apply(snapshot, animatingDifferences: true)
}
}
}
}
I want to use the same objects of one ViewController for saving into Firebase and for fetching saved data to preview and update if necessary.
Initially I used textfield in static cells it worked pretty well, but fail to insert text in textfield in dynamic cell.
When I call print function for the textfield in console it prints out correct value, but doesn't show anything on screen of simulator. I even tried to use simple strait text string to put it into textfield, but unsuccessful.
here is related code from TextMessageViewController, which i use for sending data to Firebase through textfields in dynamical tablecells
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: TextInputTableViewCell = receiverEmailTableView.dequeueReusableCell(withIdentifier: "ReceiverEmail") as! TextInputTableViewCell!
cell.recepientEmailTF.delegate = self
cell.recepientEmailTF.tag = indexPath.row
return cell
}
func textFieldDidEndEditing(_ textField: UITextField, reason: UITextFieldDidEndEditingReason) {
if MyGlobalVariables.emails.count <= 3 {
print("tag master = \(textField.tag)")
switch textField.tag {
case 0:
if MyGlobalVariables.emails.endIndex == 0 {
MyGlobalVariables.emails.append(textField.text!)
}
MyGlobalVariables.emails[0] = textField.text!
case 1:
if MyGlobalVariables.emails.endIndex == 1 {
MyGlobalVariables.emails.append(textField.text!)
}
MyGlobalVariables.emails[1] = textField.text!
case 2:
if MyGlobalVariables.emails.endIndex == 2 {
MyGlobalVariables.emails.append(textField.text!)
}
MyGlobalVariables.emails[2] = textField.text!
default:
print("exceeded")
}
DispatchQueue.main.async {
self.receiverEmailTableView.reloadData()
}
} else {
print("exceeded emails limit, add alert")
}
}
Portion of code from TextPreviewViewController from where I want to get firebase data and add it to texfields. This viewcontroller is connected to preview viewcontroller in storyboard
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let edit = UITableViewRowAction(style: .default, title: "Edit") { (action, indexPath) in
let newMessageVC = self.storyboard?.instantiateViewController(withIdentifier: "TextMessage") as? TextMessageViewController
newMessageVC?.modalPresentationStyle = .overCurrentContext
self.present(newMessageVC!, animated: true, completion: {
let updateButton = newMessageVC?.saveOrUpdateButton
updateButton?.titleLabel?.text = "Update"
let messageBody = newMessageVC?.messageTV
let dateField = newMessageVC?.tergetDateTF
let action = MyGlobalVariables.refMessages.child(MyGlobalVariables.uidUser!)
// CONCERN POINT: from here->
let cell1: TextInputTableViewCell = newMessageVC?.receiverEmailTableView.dequeueReusableCell(withIdentifier: "ReceiverEmail") as! TextInputTableViewCell!
cell1.recepientEmailTF.delegate = self
cell1.recepientEmailTF.allowsEditingTextAttributes = true
let texfielf = cell1.recepientEmailTF
MyGlobalVariables.emails.removeAll()
MyGlobalVariables.emails = ["","",""]
// cell1.recepientEmailTF.text = "Suka blyat" <- even this simple text doesnt appear
MyGlobalVariables.emails[0].append(self.messages[indexPath.row].email1!)
texfielf?.text = MyGlobalVariables.emails[0]
//cell1.recepientEmailTF.text = MyGlobalVariables.emails[0] <- this code also doesnt work
MyGlobalVariables.emails[1].append(self.messages[indexPath.row].email2!)
texfielf?.text = self.messages[indexPath.row].email2!
MyGlobalVariables.emails[2].append(self.messages[indexPath.row].email3!)
texfielf?.text = self.messages[indexPath.row].email3!
DispatchQueue.main.async {
newMessageVC?.receiverEmailTableView.reloadData()
}
//CONCERN POINT: ->up to here
messageBody?.text = self.messages[indexPath.row].message!
dateField?.text = self.messages[indexPath.row].setupDate!
if let autoID2 = self.messages[indexPath.row].autoID {
MyGlobalVariables.messageForUpdate1.append(autoID2) }
})
}
return [edit]
}
My UITableViewCell class
public class TextInputTableViewCell: UITableViewCell, UITextFieldDelegate {
#IBOutlet weak var recepientEmailTF: UITextField!
override public func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override public func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}}
I would appreciate any help or advices.
I am struggling on how to display a system message in a chatroom. Currently, I have already built the chatroom using MessageKit in Swift and finished the overall UI. What I want is, when I click the red button at the upper right corner, a system message "The button is tapped" will automatically display in the chatroom, like the sample picture below. Thanks a lot if anyone can help!
sample picture
This answer may be a bit late but the solution is quite simple. All you need to do is create a custom cell and configure it with the messages view controller data source.
There's a comprehensive guide you can follow on the official repo for help with what to do alongside an example
Here's some demo code (you won't be able to copy and paste this since I'm referencing some custom UIView subclasses)
Firstly, create your UICollectionViewCell
class SystemMessageCell: CollectionCell<MessageType> {
lazy var messageText = Label()
.variant(.regular(.small))
.color(.gray)
.align(.center)
override func setupView() {
addSubview(messageText)
}
override func setupConstraints() {
messageText.snp.makeConstraints { $0.edges.equalToSuperview() }
}
override func update() {
guard let item = item else { return }
switch item.kind {
case .custom(let kind):
guard let kind = kind as? ChatMessageViewModel.Kind else { return }
switch kind {
case .system(let systemMessage):
messageText.text = systemMessage
}
default:
break
}
}
}
Secondly, you need to create two separate files.
One is a size calculator and the other is a custom flow layout.
class SystemMessageSizeCalculator: MessageSizeCalculator {
override func messageContainerSize(for message: MessageType) -> CGSize {
// Just return a fixed height.
return CGSize(width: UIScreen.main.bounds.width, height: 20)
}
}
class SystemMessageFlowLayout: MessagesCollectionViewFlowLayout {
lazy open var sizeCalculator = SystemMessageSizeCalculator(layout: self)
override open func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator {
let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
if case .custom = message.kind {
return sizeCalculator
}
return super.cellSizeCalculatorForItem(at: indexPath);
}
}
Finally, hook it all up in your MessagesViewController like so:
class ChatViewController: MessagesViewController {
override func viewDidLoad() {
messagesCollectionView = MessagesCollectionView(frame: .zero, collectionViewLayout: SystemMessageFlowLayout())
super.viewDidLoad()
messagesCollectionView.register(SystemMessageCell.self)
}
// MARK: - Custom Cell
override open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
fatalError("Ouch. nil data source for messages")
}
let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
if case .custom = message.kind {
let cell = messagesCollectionView.dequeueReusableCell(SystemMessageCell.self, for: indexPath)
cell.item = message
return cell
}
return super.collectionView(collectionView, cellForItemAt: indexPath)
}
}
The end result will be something like this:
I'm having a tableView with a custom cell, whenever I click the checkbox button the value inside the cell increases i.e.,(0 to 1) in cell, and on uncheck value decreases, that works perfectly. But whenever I try to print those values from the cell to a UILabel outside tableView, the values are not changing.
This is the below code I have Used
var data = [[String: AnyObject]]()
func getDetails() {
let paymentURL = paymentListURL + String(28) + "&student_id=" + String(33)
Alamofire.request(paymentURL).responseJSON { (response) in
if ((response.result.value) != nil) {
var jsonVar = JSON(response.result.value!)
print(jsonVar)
if let da = jsonVar["types"].arrayObject {
self.data = da as! [[String:AnyObject]]
}
if self.data.count > 0 {
self.tableView.reloadData()
}
}
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TabCell
cell.checkB.tag = indexPath.row
let ip = data[indexPath.row]
cell.nameText.text = ip["title"] as? String
if cell.nameText.text == "Mandatory testing" {
cell.checkB.checkState = .checked
cell.backgroundColor = UIColor.lightGray
cell.checkB.backgroundColor = UIColor.lightGray
}
if ip["mandatory"] as? String == "yes" {
moneyText.text = ip["amount"] as? String
//moneyText is UILabel outside Tableview
cell.amountValue.text = ip["amount"] as? String
cell.checkB.isEnabled = false
} else {
moneyText.text = "0"
if cell.amountValue.text == "1"{
print("ONE")
}
}
return cell
}
func didPressButton(_ tag: Int) {
let indexPath = IndexPath.init(row: 0, section: 0)
let cell = tableView.cellForRow(at: indexPath) as! TabCell
moneyText.text = String(cell.someValue)
}
And for TableviewCell I Used
protocol TabCellDelegate {
func didPressButton(_ tag: Int)
}
class TabCell: UITableViewCell {
#IBOutlet weak var checkB: M13Checkbox!
#IBOutlet weak var nameText: UILabel!
#IBOutlet weak var amountValue: UILabel!
var someValue: Int = 0 {
didSet {
amountValue.text = "\(someValue)"
}
}
#IBAction func checkBAction(_ sender: M13Checkbox) {
cellDelegate?.didPressButton(sender.tag)
if checkB.checkState == .checked {
someValue += 1
} else if checkB.checkState == .unchecked {
someValue -= 1
}
}
}
I tried first adding those values from cell to an Array, and then adding all the values in array and printing in UILabel, but the values are not changing, it was only incrementing.i.e., even after unchecking the checkbox the value is increasing.
I tried even using protocol it did not work for me
Any Help will be appreciated.
You are updating someValue from the checkBAction handler inside the TabCell. The property didSet handler will then update the amountValue label. This is the reason, why the cell's label is being updated.
You do not have any code that updates the moneyText after someValue changed. You only set moneyText.text from tableView(_:cellForRow:), but this is called when a cell is being displayed, maybe multiple times after scrolling etc.
You could do the following:
Create a delegate property inside the cell (use a custom protocol as type)
Set the controller to be that delegate
When the value changes, call a function of that delegate (e.g. call the controller)
Inside that, update the moneyText
As an idea (might not compile because I don't have all your classes):
protocol MyTabCellProtocol {
func checkboxChanged(_ checkbox:M13Checkbox, atRow:Integer)
}
class TabCell: UITableViewCell {
weak delegate:MyTabCellProtocol?
// ...
#IBAction
func checkBAction(_ sender: M13Checkbox) {
if checkB.checkState == .checked {
someValue += 1
} else if checkB.checkState == .unchecked {
someValue -= 1
}
delegate?.checkboxChanged(self, checkB.tag)
}
}
class MyController : UIViewController, MyTabCellProtocol {
func checkboxChanged(_ checkbox:M13Checkbox, atRow:Integer) {
moneyText.text = "\(checkbox.someValue)"
}
}
But if I think further, I would suggest to refactor the whole code a little. The problem I see is that your action handler inside the cell does update the someValue property of the cell, but does not update the outside model (ip["amount"]). I think what you should do is:
Inside the cell's checkBAction, just call the delegate and provide the information about the row that has been modified (self.checkB.tag) and the check state. Do not update the amountValue here!
In the delegate implementation, update the model ip["amount"]
Call reloadRows(at:with:) of the table view to refresh the cell
Then, cellForRow is automatically being called, in which you then update the cell and the outer label.