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.
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'm going to try to make my attempt make sense in writing.
I used code rather than the storyboard for the tableview cells and uiswitch. The number of cells is based on count. How do I select what happens when the value changes for a uiswitch that is inside the different cells.
I feel like I need the function didselectrow but I don't know how to access it's uiswitch. As you can see in the handleswitchaction function each uiswitch does the action.
class SettingsCell: UITableViewCell {
lazy var switchControl: UISwitch = {
let switchControl = UISwitch()
switchControl.isOn = true
switchControl.onTintColor = UIColor(red: 55/255, green: 130/255, blue: 250/255, alpha: 1)
switchControl.translatesAutoresizingMaskIntoConstraints = false
switchControl.addTarget(self, action: #selector(handleSwitchAction), for: .valueChanged)
return switchControl
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
addSubview(switchControl)
switchControl.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
switchControl.rightAnchor.constraint(equalTo: rightAnchor, constant: -12).isActive = true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
//fatalError("init(coder:) has not been implemented")
}
#objc func handleSwitchAction(sender: UISwitch) {
if sender.isOn {
print("Is on")
} else {
print("Is off")
}
}
}
Inside your dequeueReusableCell(withIdentifier:for:) add tag to your cell
settingsCell.switchControl.tag = indexPath.row
And use it inside handleSwitchAction as
switch(sender.tag) {
case 0: // First row
case 1: // Second row
// ...
}
Tag property is available in any UIView: https://developer.apple.com/documentation/uikit/uiview/1622493-tag
You can just switch indexPath.row without tags if your content is static
in cell for row at:
switch indexPath.row {
case 0:
...
}
or if it is dynamic you need to get some properties from backend to know how to handle.
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 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'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