I have a prototype cell. When the table view controller is opened for the first time. the cell looks like this:
Question #1: I have a constraint that the leading space to superview of calendar icon should be 10 points. but in reality it's 16 points.
Then very strange thing happens. I click on the cell. It goes to the next screen. Then I come back to this screen. And the icon is positioned correctly (10points space):
Then what happens next is: if i click on the cell, for a very short moment (when highlighted) the icon moves to the right for 6 points:
Question #2: how to avoid this 'moving' ?
What I tried to do: added these 2 lines to didSelectRowAtIndexPath and willSelectRowAtIndexPath
cell?.setNeedsLayout()
cell?.setNeedsUpdateConstraints()
But it doesn't help much.. Please let me know if you need more details. Btw, I have no code that changes any constraints. Everything is static.
EDIT: this is how constraints of icon (imageView) looks like:
note: leading space to superview equals to 2 because it has some default of 8 (so 2+8=10).
Please adapt and try my code. Before register this cell
tableView.register(UserCell.self, forCellReuseIdentifier: "myCell")
and initialize
let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as! UserCell
======
class UserCell: UITableViewCell {
override func layoutSubviews() {
super.layoutSubviews()
textLabel?.frame = CGRect(x: 56, y: (textLabel?.frame.origin.y)! - 2, width: (textLabel?.frame.width)!, height: (textLabel?.frame.height)!)
detailTextLabel?.frame = CGRect(x: 56, y: (detailTextLabel?.frame.origin.y)! + 2, width: (detailTextLabel?.frame.width)!, height: (detailTextLabel?.frame.height)!)
}
let profileImagView: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.layer.cornerRadius = 20
imageView.layer.masksToBounds = true
imageView.image = UIImage(named: "logo")
imageView.contentMode = .scaleAspectFill
return imageView
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
addSubview(profileImagView)
profileImagView.leftAnchor.constraint(equalTo: self.leftAnchor,constant:8).isActive = true
profileImagView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
profileImagView.widthAnchor.constraint(equalToConstant: 40).isActive = true
profileImagView.heightAnchor.constraint(equalToConstant: 40).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I have not tried through the xib, however I think it makes sense
Related
So I have a custom cell class sitting in my table view with stack views that contain UIImageViews. I want to add tap gesture recognition to the symbols, however the gesture isn't recognized for ONLY subviews of the custom cell (it works fine for the view itself). My cell code looks like:
class MonthlyViewCell: UITableViewCell {
private let someSymbol: UIImageView = {
guard let image = UIImage(systemName: "j.circle.fill") else {return nil}
let config = UIImage.SymbolConfiguration(pointSize: 27)
let newImage = image.applyingSymbolConfiguration(config)
let view = UIImageView(image: newImage)
view.frame = CGRect(x: 0, y: 0, width: 10, height: 10)
view.contentMode = .scaleAspectFit
view.tag = 1
view.isUserInteractionEnabled = true
return view
}()
.
.
// other symbols
private var delegate: MonthlyViewCellDelegate?
private var stackViewOne = UIStackView()
private var stackViewTwo = UIStackView()
private var stackViewThree = UIStackView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
private func commonInit() {
bounds = CGRect(x: 0, y: 0, width: 320, height: 120)
frame = bounds
print(frame)
configureSubViews()
addSubview(stackViewOne)
addSubview(stackViewTwo)
addSubview(stackViewThree)
//addTargetToSymbols()
activateConstraints()
stackViewOne.frame = CGRect(x: 0, y: 0, width: frame.width-1, height: 15)
stackViewTwo.frame = CGRect(x: 0, y: 0, width: frame.width-1, height: 15)
stackViewThree.frame = CGRect(x: 0, y: 0, width: frame.width-1, height: 15)
let gestureRecognizer = UITapGestureRecognizer()
gestureRecognizer.addTarget(self, action: #selector(testTapped(sender:)))
stackViewTwo.addGestureRecognizer(gestureRecognizer)
}
private func configureSubViews() {
stackViewOne.translatesAutoresizingMaskIntoConstraints = false
stackViewTwo.translatesAutoresizingMaskIntoConstraints = false
stackViewThree.translatesAutoresizingMaskIntoConstraints = false
stackViewOne.axis = .horizontal
stackViewTwo.axis = .horizontal
stackViewThree.axis = .horizontal
stackViewOne.spacing = 60
stackViewTwo.spacing = 60
stackViewThree.spacing = 60
let viewsOne = [januarySymbol, februarySymbol, marchSymbol, aprilSymbol]
let viewsTwo = [maySymbol, juneSymbol, julySymbol, augustSymbol]
let viewsThree = [septemberSymbol, octoberSymbol, novemberSymbol, decemberSymbol]
let stackViews = [stackViewOne,stackViewTwo,stackViewThree]
for one in viewsOne {
stackViewOne.addArrangedSubview(one!)
}
for two in viewsTwo {
stackViewTwo.addArrangedSubview(two!)
}
for three in viewsThree {
stackViewThree.addArrangedSubview(three!)
}
/*for view in stackViews {
bringSubviewToFront(view)
}*/
}
private func activateConstraints() {
let array: [NSLayoutConstraint] = [
stackViewOne.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
stackViewTwo.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
stackViewThree.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
stackViewOne.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 25),
stackViewTwo.topAnchor.constraint(equalTo: stackViewOne.bottomAnchor, constant: 25),
stackViewThree.topAnchor.constraint(equalTo: stackViewTwo.bottomAnchor, constant: 25)
]
NSLayoutConstraint.activate(array)
}
private func addTargetToSymbols() {
let gestureRecognizer = UITapGestureRecognizer()
let symbols: [UIImageView?] = [januarySymbol, februarySymbol, marchSymbol, aprilSymbol, maySymbol, juneSymbol, julySymbol, augustSymbol, septemberSymbol, novemberSymbol, decemberSymbol]
for symbol in symbols {
guard let symbol = symbol else {return}
gestureRecognizer.addTarget(self, action: #selector(monthSymbolTapped(sender:)))
symbol.addGestureRecognizer(gestureRecognizer)
bringSubviewToFront(symbol)
}
}
#objc func testTapped(sender: UITapGestureRecognizer) {
print("tapped")
}
func setDelegate(delegate: MonthlyViewCellDelegate) {
self.delegate = delegate
}
}
And the delegate methods are in the vc. I have tried setting the frames of each subview to be contained within the frame of their superviews. I should also mention that I set a breakpoint at the target methods to be called and none of them are triggered, so it seems that the methods aren't being called.
Also isUserInteractionEnabled is set to true for the view and its subviews. translatesAutoresizingMaskIntoConstraints is set to false for all subviews but NOT for the view itself, as this conflicts with the views auto constraints with the tableview it is contained in.
One possibility is it may have something to do with the constraints I set causing the stackviews respective frames to exceed the frame of its superview, however I set the stackviews's frames AFTER the constraints are activated, so this seems unlikely as well.
Finally I should mention that that the UIViewController has its view assigned to a .UITableView property which contains the cell which is initialized in cellForRowAt.
EDIT: As mentioned in the comments it is possible that using only one UITapGestureRecognizer instance in addTargetsToSymbols()for multiple subviews was the issue, so I made the following adjustment;
private func addTargetToSymbols() {
let symbols: [UIImageView?] = [januarySymbol, februarySymbol, marchSymbol, aprilSymbol, maySymbol, juneSymbol, julySymbol, augustSymbol, septemberSymbol, novemberSymbol, decemberSymbol]
for symbol in symbols {
guard let symbol = symbol else {return}
let gestureRecognizer = UITapGestureRecognizer()
gestureRecognizer.addTarget(self, action: #selector(monthSymbolTapped(sender:)))
symbol.addGestureRecognizer(gestureRecognizer)
bringSubviewToFront(symbol)
}
}
moving the instance into the for-in loop, so that a new unique instance is used for each loop. This did not work. Additionally I imagine my attempt to test this on stackViewTwo individually would've worked if it was true.
As requested in the comments; here is the code for monthSymbolTapped()
#objc func monthSymbolTapped(sender: UIGestureRecognizer) {
print("tst")
guard let tag = sender.view?.tag else {return}
switch tag {
case 1:
delegate?.pushToYearlyVisits(month: tag)
// other cases
default:
return
}
}
NOTE: addTargetsToSymbols() was commented out while trying to see if simply adding gesture recognition to one of the stack views would yield a different result. Otherwise addTargetsToSymbols() has not been commented out on my local machine when troubleshooting this problem.
Here is a version of your custom cell class that works just fine with each image view having its own tap gesture.
I cleaned up the code a lot. I created a main stack view that contains the 3 stack views you had with each of those containing 4 of the image views.
I eliminated most of the attempts to set frames since constraints are being used.
I think your main issue was having incorrect constraints for the stack views which resulted in most of the image views being outside of the frame's cell which prevented the gestures from working. You should also be adding subviews to the cell's contentView, not the cell itself.
I also changed how the image views are created so I could get a fully working demo since your code was not complete. Obviously you can use your own code for all of the symbols if you prefer.
With the updated code below, tapping on any of the 12 images views results in the "tapped" message being printed.
You should also set your table view's rowHeight property to UITableView.automaticDimension to ensure the correct row height.
class MonthlyViewCell: UITableViewCell {
private static func monthImage(_ letter: String) -> UIImageView {
let image = UIImage(systemName: "\(letter).circle.fill")!
let config = UIImage.SymbolConfiguration(pointSize: 27)
let newImage = image.applyingSymbolConfiguration(config)
let view = UIImageView(image: image)
view.frame = CGRect(x: 0, y: 0, width: 10, height: 10)
view.contentMode = .scaleAspectFit
view.isUserInteractionEnabled = true
return view
}
private let months: [UIImageView] = ["j", "f", "m", "a", "m", "j", "j", "a", "s", "o", "n", "d"].map { MonthlyViewCell.monthImage($0) }
private var mainStack = UIStackView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
private func commonInit() {
configureSubViews()
contentView.addSubview(mainStack)
addTargetToSymbols()
activateConstraints()
}
private func configureSubViews() {
mainStack.axis = .vertical
mainStack.distribution = .equalCentering
mainStack.alignment = .fill
mainStack.spacing = 40
mainStack.translatesAutoresizingMaskIntoConstraints = false
for i in stride(from: 0, to: 12, by: 4) {
let stack = UIStackView(arrangedSubviews: Array(months[i..<i+4]))
stack.axis = .horizontal
stack.distribution = .equalSpacing
stack.alignment = .top
stack.spacing = 60
mainStack.addArrangedSubview(stack)
}
}
private func activateConstraints() {
NSLayoutConstraint.activate([
mainStack.topAnchor.constraint(equalTo: contentView.topAnchor),
mainStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
mainStack.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 25),
mainStack.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -25),
])
}
private func addTargetToSymbols() {
for symbol in months {
let gestureRecognizer = UITapGestureRecognizer()
gestureRecognizer.addTarget(self, action: #selector(monthSymbolTapped(sender:)))
symbol.addGestureRecognizer(gestureRecognizer)
}
}
#objc func monthSymbolTapped(sender: UITapGestureRecognizer) {
if sender.state == .ended {
print("tapped")
}
}
}
I am going to create a dictionary app with a list of words and their translations. My idea is next - when to click / touch the word (text on the UIButton) - the word will be voiced. But I see the issue with scrolling the UITableView - I can scroll it only if I touch the delimiter between table rows
let srollView = UIScrollView()
let tableRect = CGRect(x: 0, y: 0, width: listContentWidth, height: listContentHeight)
myTableView = UITableView(frame: tableRect, style: UITableView.Style.plain)
myTableView.register(ListTableViewCell.self, forCellReuseIdentifier: cellId) //Registration of my Custom ListTableViewCell
myTableView.dataSource = self
myTableView.delegate = self
myTableView.delaysContentTouches = true;
myTableView.canCancelContentTouches = true;
srollView.addSubview(myTableView)
How to use UIButtons inside of the UITableViewCell. which I create programmatically:
class ListTableViewCell: UITableViewCell {
private let textBtn: UIButton = {
let btn = UIButton()
//...
return btn
}()
private let translationBtn: UIButton = {
let btn = UIButton()
//...
return btn
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let stackView = UIStackView(arrangedSubviews: [
textBtn,
translationBtn
])
stackView.alignment = .fill
stackView.distribution = .fillEqually
stackView.axis = .horizontal
stackView.spacing = 5
stackView.translatesAutoresizingMaskIntoConstraints = false
//...
addSubview(stackView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
You need to add target for that button.
textBtn.addTarget(self, action: #selector(yourFunction(sender:)), for: .touchUpInside)
And of course you need to set tag of that button since you are using it.
textBtn.tag = indexPath.row
To get the tag in your function:
#objc func yourFunction(sender: UIButton){
let buttonTag = sender.tag
}
Some things to consider:
You need to add the subview to the cells contentView and not to the cell itself.
Cells are being reused: so subclass UITableViewCell and add the button to its initializer.
I am trying to make it so that there is a 'Reset Progress' button inside the centre of a cell. Other posts I have found only show how to add a button on the left side or the right side, or require prior steps in the Interface Builder.
How can I add a button in the centre of a cell programmatically without any steps in the Interface Builder?
class YourCell: UITableViewCell {
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
addSubview(button)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
lazy var button: UIButton = {
let button = UIButton()
button.backgroundColor = .blue
button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
return button
}()
override func layoutSubviews() {
super.layoutSubviews()
button.frame = CGRect(x: 0, y: 0, width: 150, height: 50)
button.center = self.center
}
#objc func buttonAction() {
print("Button has been pressed")
}
}
Download a library called "TinyConstraints" which is a standard in iOS App Development.
Import it into your SWIFT File.
Add a subview of that button to your tableView Cell.
Then try something like: Button.centreXAxisInSuperView() or YAxis if you need be, or both... that'll center it for sure.
I want to create an hourly calendar view that is relatively basic, but similar to Apple's native calendar view. How do you add labels to be in line with the row/cell separators, and not contained in a cell. Like this:
Is there a property that lets you add a label to the lines? Do the labels have to be placed outside of the table view? Or is there a separate table that occurs?
In terms of creating colored blocks to represent events on the calendar, what would be the best way to go about doing this? Would it just be a CGRect in a prototype cell? Would you need to create separate xib files?
Thanks in advance for the help, I am still new to learning Swift!
It's not possible (or technically, it would be possible, but the overhead is too high, considering your other options).
Instead of using cell separators, set separatorStyle = .none, and draw the line in the cell (e.g., as a UIView with view.height = 1 and view.backgroundColor = .grey) and normally add the label in the cell.
Basically the solution is very simple: disable standard separator lines, and rather draw separator inside the cell (bottom or top) along with the labels. That's how I've been doing things when the client asked for some custom fancy separators - I added a custom line at the bottom of the cell and used the rest of the cell's contentView as for the cell's content.
EDIT
You can use a following example to start with (note that this is just one of several different approaches how to manage it):
class TimeCellViewController: UITableViewController {
override func loadView() {
super.loadView()
// you can use UITableViewAutomaticDimension instead of static height, if
// there will be variable heights that you don't know upfront
// https://stackoverflow.com/a/18746930/2912282
// or mine:
// https://stackoverflow.com/a/47963680/2912282
tableView.rowHeight = 80
tableView.estimatedRowHeight = 80
tableView.separatorStyle = .none
// to allow scrolling below the last cell
tableView.tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 40))
tableView.register(TimeCell.self, forCellReuseIdentifier: "timeCell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 24
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "timeCell", for: indexPath) as! TimeCell
if indexPath.row > 0 {
cell.topTime = "\(indexPath.row):00"
} else {
cell.topTime = ""
}
cell.bottomTime = "\(indexPath.row + 1):00"
return cell
}
}
class TimeCell: UITableViewCell {
// little "hack" using two labels to render time both above and below the cell
private let topTimeLabel = UILabel()
private let bottomTimeLabel = UILabel()
private let separatorLine = UIView()
var topTime: String = "" {
didSet {
topTimeLabel.text = topTime
}
}
var bottomTime: String = "" {
didSet {
bottomTimeLabel.text = bottomTime
}
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
contentView.addSubview(topTimeLabel)
contentView.addSubview(bottomTimeLabel)
contentView.addSubview(separatorLine)
topTimeLabel.textColor = UIColor.gray
topTimeLabel.textAlignment = .right
bottomTimeLabel.textColor = UIColor.gray
bottomTimeLabel.textAlignment = .right
separatorLine.backgroundColor = UIColor.gray
bottomTimeLabel.translatesAutoresizingMaskIntoConstraints = false
topTimeLabel.translatesAutoresizingMaskIntoConstraints = false
separatorLine.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
bottomTimeLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 0),
bottomTimeLabel.centerYAnchor.constraint(equalTo: self.bottomAnchor),
bottomTimeLabel.widthAnchor.constraint(equalToConstant: 50),
topTimeLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 0),
topTimeLabel.centerYAnchor.constraint(equalTo: self.topAnchor),
topTimeLabel.widthAnchor.constraint(equalToConstant: 50),
separatorLine.leftAnchor.constraint(equalTo: bottomTimeLabel.rightAnchor, constant: 8),
separatorLine.bottomAnchor.constraint(equalTo: self.bottomAnchor),
separatorLine.heightAnchor.constraint(equalToConstant: 1),
separatorLine.rightAnchor.constraint(equalTo: self.rightAnchor, constant: 0),
])
// if you use UITableViewAutomaticDimension instead of static height,
// you will have to set priority of one of the height constraints to 999, see
// https://stackoverflow.com/q/44651241/2912282
// and
// https://stackoverflow.com/a/48131525/2912282
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I need a cell with a label that takes up the entire contentView. I've done this a bunch of times with a nib but this time I decided to avoid the creating a nib. Below is my code.
class StackLegendCell: UITableViewCell {
var title = UILabel()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(title)
title.numberOfLines = 1
title.textAlignment = .center
title.setContentCompressionResistancePriority(1000, for: .horizontal)
title.setContentHuggingPriority(1000, for: .horizontal)
title.text = "??"
//contentView.translatesAutoresizingMaskIntoConstraints = false
title.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 1.0).isActive = true
title.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: 1.0).isActive = true
}
The problem is that the label doesn't show anything. A check into view debugger shows that width = height = 0. But I'm setting height and width anchors which should give a size to be the same as the contentView.
What am I missing?
Thanks.
Finally, I found the solution. You missed setting the frame of the titleLabel
title.frame = contentView.frame
add this line of code and build. This should work.