Layout constraint not updating for UITableViewCell - ios

The effect I'm tying to achieve is making it feel like the UITableViewCell "widens" when it is selected. I do this by adding a subview (let's call it visibleView) to the UITableViewCell's content view, and then I adjust mainView when the cell is selected.
However, visibleView's size doesn't change upon selection. Code below:
class feedTableCell: UITableViewCell {
var visibleCell: UIView!
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.backgroundColor = .clear
self.visibleCell = UIView()
self.visibleCell.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(self.visibleCell)
visibleCell.widthAnchor.constraint(equalToConstant: 150).isActive = true
visibleCell.heightAnchor.constraint(equalToConstant: 100).isActive = true
visibleCell.centerXAnchor.constraint(equalTo: self.contentView.centerXAnchor).isActive = true
visibleCell.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor).isActive = true
visibleCell.layer.cornerRadius = 15
visibleCell.backgroundColor = .white
}
override func setSelected(_ selected: Bool, animated: Bool) {
if selected {
self.visibleCell.widthAnchor.constraint(equalToConstant: 300).isActive = true
self.contentView.layoutIfNeeded()
} else {
self.visibleCell.widthAnchor.constraint(equalToConstant: 150).isActive = true
self.contentView.layoutIfNeeded()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
However, if I replace layout constraints with frames and instead update visibleCell by adjusting its frame, everything works fine.

You have to remove the previous width constraint before you add a new one. They are not exchanged. Hold a reference to the constraint to be able to remove it before you add the new constraint.

Related

Hosting cell with Swift UI cell views are overlapping on scroll

I am trying to add a SwiftUI view as a cell view using a hosting cell. I am setting UITableViewAutomaticDimension for the height of the cell.
During the scroll, the cells overlaps.
My understanding is that it could be due to the deque. Is there a way to handle this?
Can anyone please help?
private func cellView(_ index: Int) -> UITableViewCell {
guard let filterCell = tableView.dequeueReusableCell(withIdentifier: "HostingCell<CellView>") as? HostingCell<CellView>,
let viewModel = viewModel.data else {
return HostingCell<cellView>()
}
let cellViewModel = viewModel.viewModelForRadioButton(at: index, theme: theme)
filterCell.set(rootView: FilterCellView(viewModel: cellViewModel, isSelected: viewModel.selectedIndex() == index), parentController: self)
return filterCell
}
class HostingCell <Content: View>: UITableViewCell {
private let hostingController = UIHostingController<Content?>(rootView: nil)
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func set(rootView: Content,
parentController: UIViewController,
hostingControllerBackground: UIColor? = nil) {
self.hostingController.rootView = rootView
self.hostingController.view.backgroundColor = hostingControllerBackground
self.hostingController.view.invalidateIntrinsicContentSize()
let requiresControllerMove = hostingController.parent != parentController
if requiresControllerMove {
parentController.addChild(hostingController)
}
if !self.contentView.subviews.contains(hostingController.view) {
self.contentView.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
hostingController.view.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor).isActive = true
hostingController.view.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor).isActive = true
hostingController.view.topAnchor.constraint(equalTo: self.contentView.topAnchor).isActive = true
hostingController.view.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor).isActive = true
}
if requiresControllerMove {
hostingController.didMove(toParent: parentController)
}
}
}
Yes, there are issues with constraints when SwiftUI and UIKit work together.
I don't have the right solution to it. But try giving hostingController.view.setContentHuggingPriority(.required, for: .vertical). Also UIHostingConfiguration will also helps if we support iOS 16+ :)
Also found some related answers
How to use a SwiftUI view in place of table view cell

UITextView not displaying keyboard inside CollectionViewCell

I am experiencing some difficulty in getting a UITextView to display a keyboard when tapped by the User. I am testing my app on a live device and not the simulator, so it is not the common "keyboard in simulator" issue a lot of developers experience.
I believe it might have something to do with the structure of the app.
I have a TableViewController with a cell that contains a CollectionViewController, and the UITextView is inside a CollectionViewCell.
I had this working in the past, but my fix does not appear to be working anymore. (I'm updating this app for the 1st time in 4 years), So I have stripped out all the code relating to other components in the cell and am now working with just this code (In order of "what should be called first):
TableViewCell for statuses:
This bit of code is inside tableView(cellForRowAtIndexPath)
case 5:
let cell = tableView.dequeueReusableCell(withIdentifier: "status") as! StatusTableViewCell
cell.collectionView.event = self.event
cell.collectionView.doLoad()
cell.collectionView.rootView = self
return cell
StatusTableViewCell
This contains the CollectionView which should display a list of "Statuses" (imagine if Facebook was horizontal)
class StatusTableViewCell: UITableViewCell {
var collectionView: StatusView
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
collectionView = StatusView(frame: .zero, collectionViewLayout: layout)
super.init(style: style, reuseIdentifier: reuseIdentifier)
doSetup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func doSetup(){
//backgroundColor = .black
addSubview(collectionView)
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[v0]|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["v0": collectionView]))
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[v0]|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["v0": collectionView]))
}
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
}
}
StatusView
This is the bit of code that calls the NewStatusViewCell
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
print("=====INDEX===== ", indexPath.row - 1)
print("number of statuses: ", statuses.count)
if(indexPath.row == 0){
print("creating newStatus")
let cell = dequeueReusableCell(withReuseIdentifier: "NewStatusCell", for: indexPath) as! NewStatusViewCell
// cell.event = self.event
return cell
}
NewStatusViewCell
This should always be called as the very first cell, and let's users post new statuses
class NewStatusViewCell: UICollectionViewCell, UITextViewDelegate {
let status = UITextView()
var event: Event? = nil
let placeholder = UILabel(frame: .zero)
let post = UIButton(type: .custom)
let tutorialView = UIView(frame: .zero)
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
status.frame = .zero
status.translatesAutoresizingMaskIntoConstraints = false
addSubview(status)
status.topAnchor.constraint(equalTo: topAnchor, constant: 10).isActive = true
status.leftAnchor.constraint(equalTo: leftAnchor, constant: 10).isActive = true
status.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -50).isActive = true
status.rightAnchor.constraint(equalTo: rightAnchor, constant: -5).isActive = true
status.layer.borderWidth = 1.0
status.layer.borderColor = UIColor.lightGray.cgColor
status.isEditable = true
}
The original code included using this class as a delegate, but I have commented out all the relevant delegate code to try to get the textView to run with only apple's code, however before I did this I added a print() statement to the textViewShouldBeginEditing() function. This statement never appeared in my logs indicating it was never called
I know I've just layed out ALOT of code, but if anyone can see where I've gone wrong I would really appreciate any help you can offer.
As always please don't hesitate to ask for more information.
Thanks :)
EDIT
This is what the app looks like just now, the white box underneath "invitees" is the CollectionViewCell and the box inside that is the UITextView
Very common cause is adding subViews to the cell:
addSubview(status)
instead of to the cell's content view:
contentView.addSubview(status)
This applies to both UITableViewCell and UICollectionViewCell
Worth noting: for auto-layout, subViews should also be constrained to the contentView and not to the cell itself.

Why target action in UITableViewCell gets lost?

I try to create a custom UITableViewCell with a UISwitch and without using a storyboard/.xib file.
The problem is that the action I set on the UISwitch seems to be lost because I can't trigger the selector action on switch tapped. Whereas the addTarget is set after the cell init. Also a tap on the switch does not toggle the state (no problem using a XIB file so the issue should not come from the UITableViewController)
class UISwitchTableViewCell: UITableViewCell {
let switchUI : UISwitch = {
let switchUI = UISwitch()
return switchUI
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.selectionStyle = .none
addSubview(switchUI)
switchUI.translatesAutoresizingMaskIntoConstraints = false
switchUI.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
switchUI.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor).isActive = true
switchUI.addTarget(self, action: #selector(switchChanged), for: .valueChanged)
}
#objc func switchChanged(_ sender: UISwitch){
print("switch changed")
}
required init?(coder: NSCoder) {
fatalError("NSCoder init not implemented")
}
}
Make sure you allow the selection of UITableView and UITableviewCell
tableView.allowsSelection = true
cell.selectionStyle = .default
The issue was not about the target being deallocated or something similar.
The issue was caused by the cell default textLabel (below set with yellow background) that overlapped the UISwitch, preventing from touch interactions. So the solution was to hide that default label and create my own one with trailing constraint equals to the UISwitch leadingAnchor.

Adding constraints on an existing textLabel in custom UITableViewHeaderFooterView

I wanted to add a simple counter of the number of objects in the table in the table header, next to its textLabel. So I created this class:
import UIKit
class CounterHeaderView: UITableViewHeaderFooterView {
static let reuseIdentifier: String = String(describing: self)
var counterLabel: UILabel
override init(reuseIdentifier: String?) {
counterLabel = UILabel()
super.init(reuseIdentifier: reuseIdentifier)
contentView.addSubview(counterLabel)
counterLabel.translatesAutoresizingMaskIntoConstraints = false
counterLabel.backgroundColor = .red
if let textLabel = self.textLabel{
counterLabel.leadingAnchor.constraint(equalTo: textLabel.trailingAnchor, constant: 6.0).isActive = true
counterLabel.topAnchor.constraint(equalTo: textLabel.topAnchor).isActive = true
counterLabel.heightAnchor.constraint(equalToConstant: 24.0).isActive = true
}
}
required init?(coder aDecoder: NSCoder) {
counterLabel = UILabel()
super.init(coder: aDecoder)
}
}
But running this results in the following error:
'Unable to activate constraint with anchors
<NSLayoutXAxisAnchor:0x60000388ae00 "UILabel:0x7fb8314710a0.leading">
and <NSLayoutXAxisAnchor:0x60000388ae80 "_UITableViewHeaderFooterViewLabel:0x7fb8314718c0.trailing">
because they have no common ancestor.
Does the constraint or its anchors reference items in different view hierarchies?
That's illegal.'
How can I add a constraint for my counterLabel based on the already existing textLabel? Isn't textLabel already a subview of ContentView?
You're trying to use built-in textLabel, which I'm pretty sure isn't available at the init time. Try to execute your layouting code inside layoutSubviews method, right after super call. The method could be evaluated a couple of times, so you should check if you've already layouted your view (e.g. couterLabel.superview != nil)
here's how it should looks like:
final class CounterHeaderView: UITableViewHeaderFooterView {
static let reuseIdentifier: String = String(describing: self)
let counterLabel = UILabel()
override func layoutSubviews() {
super.layoutSubviews()
if counterLabel.superview == nil {
layout()
}
}
func layout() {
contentView.addSubview(counterLabel)
counterLabel.translatesAutoresizingMaskIntoConstraints = false
counterLabel.backgroundColor = .red
if let textLabel = self.textLabel {
counterLabel.leadingAnchor.constraint(equalTo: textLabel.trailingAnchor, constant: 6.0).isActive = true
counterLabel.topAnchor.constraint(equalTo: textLabel.topAnchor).isActive = true
counterLabel.heightAnchor.constraint(equalToConstant: 24.0).isActive = true
}
}
}

Swipe Table Cells overlapping cell content on swipe motion

I'm using MGSwipeTableCell in swift, but have tried multiple other libraries, all resulting in the same problem.
Basically, I set up a custom cell class, of the type MGSwipeTableCell. I add some labels, etc, and this all works well. See code below for Cell Class Code.
import UIKit
import BTLabel
import MGSwipeTableCell
class MessageCell: MGSwipeTableCell {
let name = UILabel()
let contactTime = BTLabel()
let lineSeperator = UIView()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
override func layoutSubviews() {
super.layoutSubviews()
self.backgroundColor = Styles().heyGreen()
self.selectionStyle = .None
name.frame = CGRectMake(self.bounds.width/10, self.bounds.height/5, self.bounds.width/10*7, self.bounds.height/10*5)
name.backgroundColor = UIColor.clearColor()
name.font = Styles().FontBold(30)
name.textColor = UIColor.whiteColor()
name.textAlignment = .Left
self.addSubview(name)
contactTime.frame = CGRectMake(self.bounds.width/10, self.bounds.height/10*7, self.bounds.width/10*7, self.bounds.height/10*2)
contactTime.backgroundColor = UIColor.clearColor()
contactTime.font = Styles().FontBold(15)
contactTime.textColor = Styles().heySelectedOverLay()
contactTime.verticalAlignment = .Top
contactTime.textAlignment = .Left
self.addSubview(contactTime)
lineSeperator.frame = CGRectMake(0, self.bounds.height - 1, self.bounds.width, 1)
lineSeperator.backgroundColor = Styles().heySelectedOverLay()
self.addSubview(lineSeperator)
}
}
The cellForRowMethod is as follows in my tableviewcontroller.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdendifier: String = "MessageCell"
var cell = tableView.dequeueReusableCellWithIdentifier(cellIdendifier) as! MessageCell!
if cell == nil {
tableView.registerClass(MessageCell.classForCoder(), forCellReuseIdentifier: cellIdendifier)
cell = MessageCell(style: UITableViewCellStyle.Default, reuseIdentifier: cellIdendifier)
}
cell.name.text = "heysup"
cell.contactTime.text = "100 days"
cell.delegate = self //optional
//configure left buttons
cell.leftButtons = [MGSwipeButton(title: "", icon: UIImage(named:"check.png"), backgroundColor: UIColor.greenColor())
,MGSwipeButton(title: "", icon: UIImage(named:"fav.png"), backgroundColor: UIColor.blueColor())]
cell.leftSwipeSettings.transition = MGSwipeTransition.Rotate3D
//configure right buttons
cell.rightButtons = [MGSwipeButton(title: "Delete", backgroundColor: UIColor.redColor())
,MGSwipeButton(title: "More",backgroundColor: UIColor.lightGrayColor())]
cell.rightSwipeSettings.transition = MGSwipeTransition.Rotate3D
return cell
}
The problem lies in that this is how it looks when i swipe across.
I'm not sure where it's going wrong or what it's doing. I'm also not sure if it's because i'm adding the labels to the wrong layer? I remember in obj-c you used to add things to the cell view or something to that effect...
Any advice?
I actually resolved this issue - i needed to add the labels to the contentView, not the actual view.
so the code should have been
self.contentView.addSubview(name)
for example, on the custom tableviewcell

Resources