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
}
}
}
Related
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
I have a question about the UICollection view list's separatorLayoutGuide. I saw this article and understood I need to override the function updateConstraints() in order to update the separator layout guide.
like this...
override func updateConstraints() {
super.updateConstraints()
separatorLayoutGuide.leadingAnchor.constraint(equalTo: otherView.leadingAnchor, constant: 0.0).isActive = true
}
I can see the tiny space between the cell's leading anchor and seprateguide's leading anchor like the image below and I want to fix it. (like the left side of the cell)
The problem is, however, I created a custom collection view list cell using this article and cannot change the separatorLayoutGuide leading to the custom view's leading.
I added the customListCell.separatorLayoutGuide.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true in order to position the leading of separatorLayoutGuide to the customView's leading, and I get the
"UILayoutGuide:0x2822d8b60'UICollectionViewListCellSeparatorLayoutGuide'.leading"> and <NSLayoutXAxisAnchor:0x280e9cac0 "ContentView:0x15960db90.leading"> because they have no common ancestor. Does the constraint or its anchors reference items in different view hierarchies? That's illegal.'
error.
After I've done the research, I figured I didn't addSubview for the separatorLayoutGuide, but even if I add a subview to the custom view, the app crashes. Is there a way to change the separator guide's leading anchor when using the custom UIView?
class CustomListCell: UICollectionViewListCell {
var item: TestItem?
override func updateConfiguration(using state: UICellConfigurationState) {
// Create new configuration object
var newConfiguration = ContentConfiguration().updated(for: state)
newConfiguration.name = item.name
newConfiguration.state = item.state
// Set content configuration
contentConfiguration = newConfiguration
}
}
struct ContentConfiguration: UIContentConfiguration, Hashable {
var name: String?
var state: String?
func makeContentView() -> UIView & UIContentView {
return ContentView(configuration: self)
}
func updated(for state: UIConfigurationState) -> Self {
guard let state = state as? UICellConfigurationState else {
return self
}
// Updater self based on the current state
let updatedConfiguration = self
if state.isSelected {
print("is selected")
} else {
print("is deselected")
}
return updatedConfiguration
}
}
class ContentView: UIView, UIContentView {
let contentsView = UIView()
let customListCell = CustomListCell()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.text = ""
return label
}()
lazy var statusLabel: UILabel = {
let label = UILabel()
label.text = ""
return label
}()
lazy var symbolImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
return imageView
}()
init(configuration: ContentConfiguration) {
// Custom initializer implementation here.
super.init(frame: .zero)
setupAllViews()
apply(configuration: configuration)
}
override func updateConstraints() {
super.updateConstraints()
customListCell.separatorLayoutGuide.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var currentConfiguration: ContentConfiguration!
var configuration: UIContentConfiguration {
get {
currentConfiguration
}
set {
guard let newConfiguration = newValue as? ContentConfiguration else {
return
}
apply(configuration: newConfiguration)
}
}
func setupAllViews() {
// add subviews and add constraints
}
func apply(configuration: ContentConfiguration) {
}
}
by overriding updateConstraints in a UICollectionViewListCell subclass
In your code you have a customListCell instance variable which is not necessary. Also you should not add separatorLayoutGuide as a subview anywhere.
Try to access the cell's contentView:
override func updateConstraints() {
super.updateConstraints()
if let customView = cell.contentView as? ContentView {
separatorLayoutGuide.leadingAnchor.constraint(equalTo: customView.leadingAnchor, constant: 0.0).isActive = true
}
}
Like this you can access any subview in your ContentView.
Another question is: Is it necessary to create a new constraint every time updateConstraints is called? Are constraints from previous calls still there? According to the documentation of updateConstraints:
Your implementation must be as efficient as possible. Do not deactivate all your constraints, then reactivate the ones you need. Instead, your app must have some way of tracking your constraints, and validating them during each update pass. Only change items that need to be changed. During each update pass, you must ensure that you have the appropriate constraints for the app’s current state.
Therefore I suggest this approach:
class ContentView: UIView, UIContentView {
var separatorConstraint: NSLayoutConstraint?
func updateForCell(_ cell: CustomListCell) {
if separatorConstraint == nil {
separatorConstraint = cell.separatorLayoutGuide.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 30)
}
separatorConstraint?.isActive = true
}
}
class CustomListCell: UICollectionViewListCell {
...
override func updateConstraints() {
super.updateConstraints()
(contentView as? AlbumContentView)?.updateForCell(self)
}
}
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.
I have a collectionView cell that should either display an image or an icon that is generated as a custom UIView (lets say IconView).
Currently, I implemented this by adding an UIImageView and an IconView as subviews to a container view.
When an image is provided, the image property of UIImageView is simply updated. When a new IconView is provided it is currently always added as a subview to the container view. Therefore, before adding, it is first checked whether an IconView has already been added, and if so it is removed.
Although this implementation works, it is not very elegant and seems not efficient since it results in scrolling issues when the number of rows increase.
Would there be a better (more efficient) way to implement this for a single CollectionViewCell?
class CustomCell: UICollectionViewCell {
internal var image: UIImage? {
didSet {
self.imageView.image = image!
}
}
internal var iconView: IconView? {
didSet {
if !(self.iconContainerView.subviews.flatMap{ $0 as? IconView}.isEmpty) {
self.iconView!.removeFromSuperview()
}
self.iconView!.translatesAutoresizingMaskIntoConstraints = false
self.iconContainerView.addSubview(self.iconView!)
self.image = nil
}
}
fileprivate var imageView: UIImageView!
fileprivate var iconContainerView: UIView!
fileprivate var layoutConstraints = [NSLayoutConstraint]()
override init(frame: CGRect) {
super.init(frame: frame)
// ContainerView
self.iconContainerView = UIView()
self.iconContainerView.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(self.iconContainerView)
// ImageView
self.imageView = UIImageView()
self.imageView.translatesAutoresizingMaskIntoConstraints = false
self.iconContainerView.addSubview(self.imageView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
self.iconContainerView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor).isActive = true
self.iconContainerView.widthAnchor.constraint(equalToConstant: 60).isActive = true
self.iconContainerView.heightAnchor.constraint(equalToConstant: 60).isActive = true
self.iconContainerView.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor).isActive = true
// Deactivate non-reusable constraints
_ = self.layoutConstraints.map { $0.isActive = false }
self.layoutConstraints = [NSLayoutConstraint]()
if let iconView = self.iconView {
self.imageView.isHidden = true
self.layoutConstraints.append(iconView.centerYAnchor.constraint(equalTo: self.iconContainerView.centerYAnchor))
self.layoutConstraints.append(iconView.centerXAnchor.constraint(equalTo: self.iconContainerView.centerXAnchor))
self.layoutConstraints.append(iconView.heightAnchor.constraint(equalToConstant: 40))
self.layoutConstraints.append(iconView.widthAnchor.constraint(equalToConstant: 40))
} else {
self.imageView.isHidden = false
self.iconView?.isHidden = true
self.layoutConstraints.append(self.imageView.leadingAnchor.constraint(equalTo: self.iconContainerView.leadingAnchor))
self.layoutConstraints.append(self.imageView.trailingAnchor.constraint(equalTo: self.iconContainerView.trailingAnchor))
self.layoutConstraints.append(self.imageView.topAnchor.constraint(equalTo: self.iconContainerView.topAnchor))
self.layoutConstraints.append(self.imageView.bottomAnchor.constraint(equalTo: self.iconContainerView.bottomAnchor))
}
_ = self.layoutConstraints.map {$0.isActive = true}
}
}
Don't ad and remove the IconView when setting. Add both in the same spot and change the isHidden, alpha, or opacity or bringSubviewToFront. This is much less main thread intensive.
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