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
Related
I realize that many people have asked this question in various forms and the answers are all over the page, so let me summarize my specific situation in hopes of getting more specific answers. First of all, I'm building for iOS 11+ and have a relatively recent version of XCode (11+). Maybe not the latest, but recent enough.
Basically, I need a self-sizing tableview where the cells may expand and collapse at runtime when the user interacts with them. In viewDidLoad I set the rowHeight to UITableView.automaticDimension and estimatedRowHeight to some number that's bigger than the canned value of 44. But the cell is not expanding like it should, even though I seem to have tried every bit of advice in the book.
If that matters, I have a custom class for the table cell but no .XIB file for it - the UI is defined directly in the prototype. I've tried a number of other variations, but it feels like the easiest is making a UIStackView the only direct child of the prototype (the "revenue" features so to speak would all be inside it. In my case, they include a label and another tableview - I nest 3 levels deep - but that's probably beside the point) and constraining all 4 of it's edges to the parent. I've tried that, and I've tinkered with the distribution in the stack view (Fill, Fill Evenly, Fill Proportionately), but none of it seems to work. What can I do to make the cells expand properly?
In case anyone's wondering, I used to override heightForRowAt but now I don't because it's not easy to predict the height at runtime and I'm hoping the process could be automated.
Start with the basics...
Here is a vertical UIStackView with two labels:
The red outline shows the frame of the stack view.
If we tap the button, it will set bottomLabel.isHidden = true:
Notice that in addition to being hidden, the stack view removes the space it was occupying.
Now, we can do that with a stack view in a table view cell to get expand/collapse functionality.
We'll start with every-other row expanded:
Now we tap the "Collapse" button for row 1 and we get:
Not quite what we want. We successfully "collapsed" the cell content, but the table view doesn't know anything about it.
So, we can add a closure... when we tap the button, the code in the cell will show/hide the bottom label AND it will use the closure to tell the table view what happened. Our cellForRowAt func looks like this:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath) as! ExpColCell
c.setData("Top \(indexPath.row)", str2: "Bottom \(indexPath.row)\n2\n3\n4\n5", isCollapsed: isCollapsedArray[indexPath.row])
c.didChangeHeight = { [weak self] isCollapsed in
guard let self = self else { return }
// update our data source
self.isCollapsedArray[indexPath.row] = isCollapsed
// tell the tableView to re-run its layout
self.tableView.performBatchUpdates(nil, completion: nil)
}
return c
}
and we get:
Here's a complete example:
Simple "dashed outline view"
class DashedOutlineView: UIView {
#IBInspectable var dashColor: UIColor = .red
var shapeLayer: CAShapeLayer!
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
shapeLayer = self.layer as? CAShapeLayer
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = 1.0
shapeLayer.lineDashPattern = [8,8]
}
override func layoutSubviews() {
super.layoutSubviews()
shapeLayer.strokeColor = dashColor.cgColor
shapeLayer.path = UIBezierPath(rect: bounds).cgPath
}
}
The cell class
class ExpColCell: UITableViewCell {
public var didChangeHeight: ((Bool) -> ())?
private let stack = UIStackView()
private let topLabel = UILabel()
private let botLabel = UILabel()
private let toggleButton = UIButton()
private let outlineView = DashedOutlineView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// button properties
toggleButton.translatesAutoresizingMaskIntoConstraints = false
toggleButton.backgroundColor = .systemBlue
toggleButton.setTitleColor(.white, for: .normal)
toggleButton.setTitleColor(.gray, for: .highlighted)
toggleButton.setTitle("Collapse", for: [])
// label properties
topLabel.text = "Top Label"
botLabel.text = "Bottom Label"
topLabel.font = .systemFont(ofSize: 32.0)
botLabel.font = .italicSystemFont(ofSize: 24.0)
topLabel.backgroundColor = .green
botLabel.backgroundColor = .systemTeal
botLabel.numberOfLines = 0
// outline view properties
outlineView.translatesAutoresizingMaskIntoConstraints = false
// stack view properties
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .vertical
stack.spacing = 8
// add the labels
stack.addArrangedSubview(topLabel)
stack.addArrangedSubview(botLabel)
// add outlineView, stack view and button to contentView
contentView.addSubview(outlineView)
contentView.addSubview(stack)
contentView.addSubview(toggleButton)
// we'll use the margin guide
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: g.topAnchor),
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
outlineView.topAnchor.constraint(equalTo: stack.topAnchor),
outlineView.leadingAnchor.constraint(equalTo: stack.leadingAnchor),
outlineView.trailingAnchor.constraint(equalTo: stack.trailingAnchor),
outlineView.bottomAnchor.constraint(equalTo: stack.bottomAnchor),
toggleButton.topAnchor.constraint(equalTo: g.topAnchor),
toggleButton.trailingAnchor.constraint(equalTo: g.trailingAnchor),
toggleButton.leadingAnchor.constraint(equalTo: stack.trailingAnchor, constant: 16.0),
toggleButton.widthAnchor.constraint(equalToConstant: 92.0),
])
// we set the bottomAnchor constraint like this to avoid intermediary auto-layout warnings
let c = stack.bottomAnchor.constraint(equalTo: g.bottomAnchor)
c.priority = UILayoutPriority(rawValue: 999)
c.isActive = true
// set label Hugging and Compression to prevent them from squeezing/stretching
topLabel.setContentHuggingPriority(.required, for: .vertical)
topLabel.setContentCompressionResistancePriority(.required, for: .vertical)
botLabel.setContentHuggingPriority(.required, for: .vertical)
botLabel.setContentCompressionResistancePriority(.required, for: .vertical)
contentView.clipsToBounds = true
toggleButton.addTarget(self, action: #selector(toggleButtonTapped), for: .touchUpInside)
}
func setData(_ str1: String, str2: String, isCollapsed: Bool) -> Void {
topLabel.text = str1
botLabel.text = str2
botLabel.isHidden = isCollapsed
updateButtonTitle()
}
func updateButtonTitle() -> Void {
let t = botLabel.isHidden ? "Expand" : "Collapse"
toggleButton.setTitle(t, for: [])
}
#objc func toggleButtonTapped() -> Void {
botLabel.isHidden.toggle()
updateButtonTitle()
// comment / un-comment this line to see the difference
didChangeHeight?(botLabel.isHidden)
}
}
and a table view controller to demonstrate
class ExpColTableViewController: UITableViewController {
var isCollapsedArray: [Bool] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(ExpColCell.self, forCellReuseIdentifier: "c")
// 16 "rows" start with every-other row collapsed
for i in 0..<15 {
isCollapsedArray.append(i % 2 == 0)
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return isCollapsedArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath) as! ExpColCell
c.setData("Top \(indexPath.row)", str2: "Bottom \(indexPath.row)\n2\n3\n4\n5", isCollapsed: isCollapsedArray[indexPath.row])
c.didChangeHeight = { [weak self] isCollapsed in
guard let self = self else { return }
// update our data source
self.isCollapsedArray[indexPath.row] = isCollapsed
// tell the tableView to re-run its layout
self.tableView.performBatchUpdates(nil, completion: nil)
}
return c
}
}
Background:
iOS14 has introduced a new way to register and configure collectionViewCell and In Lists In CollectionView WWDC video apple developer says that self sizing is default to UICollectionViewListCell and we don't have to explicitly specify the height for cells. This works great if I use system list cell in various configurations but self sizing fails when I use it with custom subclass of UICollectionViewListCell
What have I tried?
iOS 14 has introduced a new way to configure the cells, where we don't access the cells components directly to set the various UI properties rater we use content configuration and background configuration to update/configure cells. This becomes little tricky when we use custom cells.
CustomSkillListCollectionViewCell
class CustomSkillListCollectionViewCell: UICollectionViewListCell {
var skillLavel: String? {
didSet {
setNeedsUpdateConfiguration()
}
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func updateConfiguration(using state: UICellConfigurationState) {
backgroundConfiguration = SkillListViewBackgroundConfiguration.getBackgroundConfiguration(for: state)
var content = SkillListViewContentConfiguration().updated(for: state)
content.label = skillLavel
contentConfiguration = content
}
}
SkillListViewBackgroundConfiguration
struct SkillListViewBackgroundConfiguration {
#available(iOS 14.0, *)
static func getBackgroundConfiguration(for state: UICellConfigurationState) -> UIBackgroundConfiguration {
var background = UIBackgroundConfiguration.clear()
if state.isHighlighted || state.isSelected {
background.backgroundColor = UIColor.green.withAlphaComponent(0.4)
}
else if state.isExpanded {
background.backgroundColor = UIColor.red.withAlphaComponent(0.5)
}
else {
background.backgroundColor = UIColor.red.withAlphaComponent(0.9)
}
return background
}
}
SkillListViewContentConfiguration
struct SkillListViewContentConfiguration: UIContentConfiguration {
var label: String? = nil
#available(iOS 14.0, *)
func makeContentView() -> UIView & UIContentView {
return SkillListView(contentConfiguration: self)
}
#available(iOS 14.0, *)
func updated(for state: UIConfigurationState) -> Self {
guard let state = state as? UICellConfigurationState else {
return self
}
let updatedConfig = self
return updatedConfig
}
}
Finally subview SkillListView
class SkillListView: UIView, UIContentView {
var configuration: UIContentConfiguration {
get {
return self.appliedConfiguration
}
set {
guard let newConfig = newValue as? SkillListViewContentConfiguration else { return }
self.appliedConfiguration = newConfig
apply()
}
}
private var appliedConfiguration: SkillListViewContentConfiguration!
var skillNameLabel: UILabel!
#available(iOS 14.0, *)
init(contentConfiguration: UIContentConfiguration) {
super.init(frame: .zero)
self.setUpUI()
self.configuration = contentConfiguration
self.apply()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func apply() {
self.skillNameLabel.text = self.appliedConfiguration.label
}
private func setUpUI() {
self.skillNameLabel = UILabel(frame: .zero)
skillNameLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
skillNameLabel.setContentHuggingPriority(.defaultHigh, for: .vertical)
self.skillNameLabel.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(skillNameLabel)
NSLayoutConstraint.activate([
self.skillNameLabel.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor, constant: 20),
self.skillNameLabel.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor, constant: 20),
self.skillNameLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor, constant: 20)
])
}
}
And I configure it using
let skillsCellConfigurator = UICollectionView.CellRegistration<CustomSkillListCollectionViewCell, Employee> { (cell, indexPath, employee) in
cell.skillLavel = employee.individualSkil
cell.accessories = [.disclosureIndicator()]
}
Issue:
Everything else works great except height
First, you need at least 1 more constraint to guarantee satisfaction as top, bottom, and leading needs a trailing or centerX to go with them. But more so, it is probably constraining to the margins rather than the superview. Cells should automatically respect margins like safe area as long as the UICollectionView itself respects them which I think is default behavior. The layoutmarginguide for cells is significantly pushed down on the top, which can be seen from a dummy example in interfacebuilder if you check.
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
}
}
}
I am trying to create a custom cell in Eureka that display an Image. When tap the image, the image displays full screen with black background.
Usually you do it with view.addSubview(newImageView), but it does not seem like Eureka cell has view class.
Here is what I got so far for the cell:
final class ImageViewCell: Cell<ImageView>, CellType {
#IBOutlet weak var viewImage: UIImageView!
//let storage = Storage.storage().reference()
required init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func setup() {
super.setup()
//cell not selectable
selectionStyle = .none
viewImage.contentMode = .scaleAspectFill
viewImage.clipsToBounds = true
height = {return 300 }
//make userImage reconize tapping
let tapRec = UITapGestureRecognizer(target: self, action: #selector(imageTapHandler(tapGestureRecognizer:)))
viewImage.addGestureRecognizer(tapRec)
viewImage.isUserInteractionEnabled = true
}
#objc func imageTapHandler(tapGestureRecognizer: UITapGestureRecognizer) {
let imageView = tapGestureRecognizer.view as! UIImageView
let newImageView = UIImageView(image: imageView.image)
newImageView.frame = UIScreen.main.bounds
newImageView.backgroundColor = .black
newImageView.contentMode = .scaleAspectFit
newImageView.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissFullscreenImage))
newImageView.addGestureRecognizer(tap)
view.addSubview(newImageView)
}
#objc func dismissFullscreenImage(_ sender: UITapGestureRecognizer) {
sender.view?.removeFromSuperview()
}
override func update() {
super.update()
// we do not want to show the default UITableViewCell's textLabel
textLabel?.text = nil
// get the value from our row
guard let imageData = row.value else { return }
// get user image data from value
let downloadData = imageData.pictureData
viewImage.image = UIImage(data: downloadData)
}
}
I am getting "Use of unresolved identifier 'view'" when trying to addSubview.
I have tried to use contentView instead of view, and the result is like this:
Screenshot for the View
If you want to show the image in fullScreen, cell's contentView or the viewController subView that holds this cell are not the right places to add this image as subview.
A proper solution to this is to use the onCellSelection(something as shown below) callback in your container ViewController and present a new ViewController that display's only image fullscreen or whatever customization you want.
imageCell.onCellSelection({[weak self] (cell, row) in
let imageVC = DisplayViewController()
imageVC.image = cell.viewImage.image
self?.present(imageVC, animated: true, completion: nil)
})
If you want to show fullscreen only when user taps the image in cell then you should get the tapGesture callback in container ViewController and show the image as above.
Thanks for Kamran, I think I found the solution.
for Eureka rows, you can always use row.onCellSelection call back.
in my case I can do this when calling the custom row:
<<< ImageViewCellRow(){ row in
row.value = ImageView(pictureData: data!)
row.onCellSelection(showMe)
}
then create a showMe function like this:
func showMe(cell: ImageViewCell, row: (ImageViewCellRow)) {
print("tapped my ImageViewCell!")
}
now you should be able to present a full screen image as Kamran's code.
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