UITableViewCell changeable content based data coming from server - swift

I am struggling this issue which is related to UITableViewCell. I have a subclass UITableViewCell called ApplicantMessageCell and it has some subviews, labels, imageviews etc. Top part of it does not depend on the state. Just gets the data, changes labels text and imageView's image.
However for the bottom part I have completely different 3 subclasses of UIView for each state coming in. I need to show related UIView subclass at the bottom part of ApplicationMessageCell. But I could not find a way to do it.
Of course, I could create different UITableViewCell subclasses for each state but I didnot want to go that road beacuse this is just one case, I have more.
I tried to create a subclass of UIView which will behave like UILabel when it comes to resizing itself. I could not manage to it.
Lastly, I know adding each UIView subclass regarding each state and explicitly showing the one/hiding rest would solve it but I believe there are better ways to achieve this.
I did not share any code because I think this more of a theoretical question, but of course I will if anyone requests.
Thanks in advance.

Here is a quick example...
The cell class has two labels, a stack view, and 3 views (red, green, blue) with varying heights to use as the "show or not" views:
First label is constrained to the Top
Second label is constrained to the bottom of First label
stack view is constrained to the bottom of Second label and to the bottom of the cell (contentView, of course)
Three views of varying heights are then added to the stack view. Presumably, the constraints on the subviews of your different views will determine their respective heights. For this example, they are set to 40, 80 and 160.
Review the comments in the following code - it should be pretty self-explanatory:
class ApplicantMessageCell: UITableViewCell {
let titleLabel = UILabel()
let subLabel = UILabel()
let stackView = UIStackView()
let viewA = UIView()
let viewB = UIView()
let viewC = UIView()
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 {
[titleLabel, subLabel, stackView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(v)
}
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
// constrain titleLabel at top
titleLabel.topAnchor.constraint(equalTo: g.topAnchor),
titleLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
titleLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
// subLabel 8-pts below titleLabel
subLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8.0),
subLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
subLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
// stackView 8-pts below subLabel
stackView.topAnchor.constraint(equalTo: subLabel.bottomAnchor, constant: 8.0),
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
])
// constrain stackView bottom to bottom
// this will avoid auto-layout complaints while the cells are configured
let c = stackView.bottomAnchor.constraint(equalTo: g.bottomAnchor)
c.priority = .defaultHigh
c.isActive = true
// UI element properties
stackView.axis = .vertical
stackView.spacing = 8
titleLabel.backgroundColor = .yellow
subLabel.backgroundColor = .cyan
viewA.backgroundColor = .red
viewB.backgroundColor = .green
viewC.backgroundColor = .blue
// you'll be filling the views with something to determine their heights
// but here we'll just set them to 40, 80 and 160 pts
for (v, h) in zip([viewA, viewB, viewC], [40.0, 80.0, 160.0]) {
stackView.addArrangedSubview(v)
v.heightAnchor.constraint(equalToConstant: CGFloat(h)).isActive = true
}
}
func fillData(_ top: String, sub: String, showViews: [Bool]) -> Void {
titleLabel.text = top
subLabel.text = sub
// hide views as defined in showViews array
for (v, b) in zip(stackView.arrangedSubviews, showViews) {
v.isHidden = !b
}
}
}
struct ApplicationStruct {
var title: String = ""
var subTitle: String = ""
var showViews: [Bool] = [true, true, true]
}
class FarukTableViewController: UITableViewController {
var theData: [ApplicationStruct] = []
override func viewDidLoad() {
super.viewDidLoad()
for i in 0..<20 {
// cycle through views 1, 2, 3
let b1 = i % 3 == 0
let b2 = i % 3 == 1
let b3 = i % 3 == 2
let a = [b1, b2, b3]
let d = ApplicationStruct(title: "Title \(i)", subTitle: "", showViews: a)
theData.append(d)
}
// just to test, set more than one view visible in a couple cells
theData[11].showViews = [true, false, true] // red and blue
theData[12].showViews = [false, true, true] // green and blue
theData[13].showViews = [true, true, false] // red and green
theData[14].showViews = [true, true, true] // all three
tableView.register(ApplicantMessageCell.self, forCellReuseIdentifier: "cell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return theData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! ApplicantMessageCell
let d = theData[indexPath.row]
let subStr = "showViews: " + d.showViews.description
c.fillData(d.title, sub: subStr, showViews: d.showViews)
return c
}
}
Result where first row shows "ViewType1" second row shows "ViewType2" and third row shows "ViewType3" ... then the rows cycle, until we hit row "Title 11" where we've set a few rows to show more than one of the "subview types":

Related

Swift: Encapsulated Layout Height always .333 larger than view

I am building a TableView filled with a variable number of cells. One type of those cells should have a fixed height of 69.0, but xcode keeps setting its encapsulated layout height .333 larger than what I set the height to, meaning it breaks the constraints because the encapsulated layout height is 69.333. If I change my Cell's size to 70 for example, the constraint is set to 70.333. I don't understand what causes this.
Here is the Cell:
class AnnotationCell: UITableViewCell {
//Set identifier to be able to call it later on
static let identifier = "AnnotationCell"
var annotation: Annotation!
//MARK: - Configure
public func configure(annotation: Annotation) {
self.annotation = annotation
}
//MARK: - Cell Style
//Add all subviews in here
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() {
//Customize Cell
contentView.backgroundColor = colors.darkGray
contentView.layer.borderWidth = 0
//Favorite
let favoriteView = UIView()
favoriteView.backgroundColor = colors.gray
favoriteView.addBorders(edges: [.top], color: colors.lightGray, width: 1)
favoriteView.translatesAutoresizingMaskIntoConstraints = false
let favIcon = UIImageView()
let myEmoji = "👀".textToImage() //Test
favIcon.image = myEmoji
favIcon.contentMode = .scaleAspectFill
favIcon.translatesAutoresizingMaskIntoConstraints = false
favoriteView.addSubview(favIcon)
let stringStack = UIStackView()
stringStack.axis = .vertical
stringStack.spacing = 6
stringStack.translatesAutoresizingMaskIntoConstraints = false
let titleString = UILabel()
titleString.text = "Test"
titleString.textColor = colors.justWhite
titleString.font = UIFont(name: "montserrat", size: 17)
titleString.translatesAutoresizingMaskIntoConstraints = false
stringStack.addArrangedSubview(titleString)
let addressString = UILabel()
addressString.text = "Test 2"
addressString.textColor = colors.lightGray
addressString.font = UIFont(name: "montserrat", size: 14)
addressString.translatesAutoresizingMaskIntoConstraints = false
stringStack.addArrangedSubview(addressString)
favoriteView.addSubview(stringStack)
let editButton = UIButton()
editButton.tintColor = colors.lightGray
let mediumConfig = UIImage.SymbolConfiguration(pointSize: 20, weight: .semibold, scale: .medium)
let mediumEditButton = UIImage(systemName: "chevron.right", withConfiguration: mediumConfig)
editButton.setImage(mediumEditButton, for: .normal)
editButton.translatesAutoresizingMaskIntoConstraints = false
favoriteView.addSubview(editButton)
contentView.addSubview(favoriteView)
//Define Constraints
NSLayoutConstraint.activate([
favoriteView.heightAnchor.constraint(equalToConstant: 69),
favoriteView.topAnchor.constraint(equalTo: contentView.topAnchor),
favoriteView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
favoriteView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
favoriteView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
favIcon.leadingAnchor.constraint(equalTo: favoriteView.leadingAnchor, constant: 15),
favIcon.centerYAnchor.constraint(equalTo: favoriteView.centerYAnchor),
favIcon.heightAnchor.constraint(equalToConstant: 50),
favIcon.widthAnchor.constraint(equalToConstant: 50),
stringStack.leadingAnchor.constraint(equalTo: favIcon.trailingAnchor, constant: 20),
stringStack.centerYAnchor.constraint(equalTo: favoriteView.centerYAnchor),
editButton.trailingAnchor.constraint(equalTo: favoriteView.trailingAnchor, constant: -15),
editButton.centerYAnchor.constraint(equalTo: favoriteView.centerYAnchor),
])
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
I am not setting the Cell's height in the TableView at all - I've tried that and that did not make things better. The Cell is getting displayed correctly, but console throws:
[LayoutConstraints] Unable to simultaneously satisfy constraints.
(
"<NSLayoutConstraint:0x281d21ae0 UIView:0x15cca3490.height == 69 (active)>",
"<NSLayoutConstraint:0x281d21bd0 V:|-(0)-[UIView:0x15cca3490] (active, names: '|':UITableViewCellContentView:0x15cca42a0 )>",
"<NSLayoutConstraint:0x281d22120 UIView:0x15cca3490.bottom == UITableViewCellContentView:0x15cca42a0.bottom (active)>",
"<NSLayoutConstraint:0x281d28820 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x15cca42a0.height == 69.3333 (active)>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x281d21ae0 UIView:0x15cca3490.height == 69 (active)>
I have tried giving every subview a fixed height (which does not make sense with a stackview in this layout) but that did not work either. I had the same layout in a view (not TableViewCell) without any issues, so I am guessing that it has to do something with the TableView.
Here are my constraints as seen from the view hierarchy, in case someone has an eye for it. I have opened up most constraints to make it as clear as possible. As can be seen next to the red arrow, the constraint of 69.0 seems to be there and I don't see anything different. The only thing I could see causing this issue would be the "UISysmenBackGroundView" which has a view inside it without any constraints.
1/3 pt sounds like a hairline thickness on a 3x device. Could it perhaps be the separator? If they’re enabled, try turning them off to see if that fixes the issue.

Tableview y origin not animating properly when navigationItem.titleView is hidden (Swift 5)

I’m trying to get the tableView to move up when the search bar does. Take a look at the problem:
I think I see what the issue is here, but I can't think of a solution. In SearchResultsUpdating I have an animation block:
func updateSearchResults(for searchController: UISearchController) {
UIView.animateKeyframes(withDuration: 1, delay: 0, options: UIView.KeyframeAnimationOptions(rawValue: 7)) {
self.tableView.frame = CGRect(x: 20, y: self.view.safeAreaInsets.top, width:
self.view.frame.size.width-40, height: self.view.frame.size.height -
self.view.safeAreaInsets.top)
}
}
It seems to me that the animation block is only receiving the previous coordinates for the y origin, hence it is animating out of sync. I tried adding a target to the tableView, or navigationBar, or the searchBarTextField instead, but nothing worked.
Any help is appreciated, thanks!
EDIT: After implementing Shawn's second suggestion this was the result:
I can't imagine why it isn't animating smoothly now... very frustrating!
EDIT 2 - Requested Code:
class ViewController: UIViewController{
//City TableView
let cityTableView = UITableView()
let searchVC: UISearchController = {
let searchController = UISearchController(searchResultsController: nil)
searchController.obscuresBackgroundDuringPresentation = true
searchController.searchBar.placeholder = "Search"
return searchController
}()
//viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
//Do any setup for the view controller here
setupViews()
//CityViewController
setupCityViewTableView()
}
//setupViews
func setupViews(){
//NAVIGATIONBAR:
//title
title = "Weather"
//set to hidden because on initial load there is a scroll view layered over top of the CityViewTableView (code not shown here). This gets set to false when the scrollView alpha is set to 0 and the CityViewTableView is revealed
navigationController?.navigationBar.isHidden = true
navigationController?.navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
//NAVIGATION ITEM:
navigationItem.searchController = searchVC
//UISEARCHBARCONTROLLER:
searchVC.searchResultsUpdater = self
}
}
//MARK: -CityViewController Functions
extension ViewController{
//setUp TableView
func setupCityViewTableView(){
cityTableView.translatesAutoresizingMaskIntoConstraints = false
//set tableView delegate and dataSource
cityTableView.delegate = self
cityTableView.dataSource = self
//background color
cityTableView.backgroundColor = .black
//separator color
cityTableView.separatorColor = .clear
//is transparent on initial load
cityTableView.alpha = 0
//set tag
cityTableView.tag = 1000
//hide scroll indicator
cityTableView.showsVerticalScrollIndicator = false
//register generic cell
cityTableView.register(UITableViewCell.self, forCellReuseIdentifier: "cityCell")
//add subview
view.addSubview(cityTableView)
//Auto Layout
cityTableView.leadingAnchor
.constraint(equalTo: view.leadingAnchor,
constant: 20).isActive = true
cityTableView.topAnchor
.constraint(equalTo: view.topAnchor,
constant: 0).isActive = true
cityTableView.trailingAnchor
.constraint(equalTo: view.trailingAnchor,
constant: -20).isActive = true
cityTableView.bottomAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor,
constant: 0).isActive = true
}
}
//MARK: -TableView Controller
extension ViewController: UITableViewDelegate,
UITableViewDataSource{
//number of rows
func tableView(_ tableView: UITableView, numberOfRowsInSection
section: Int) -> Int {
if tableView.tag == 1000{
return 5
}
return self.models[tableView.tag].count
}
//cell for row
func tableView(_ tableView: UITableView, cellForRowAt indexPath:
IndexPath) -> UITableViewCell {
//CityViewController
if tableView.tag == 1000{
let cell = tableView.dequeueReusableCell(withIdentifier:
"cityCell", for: indexPath)
cell.textLabel?.text = "Test"
cell.textLabel?.textAlignment = .center
cell.backgroundColor = .systemGray
cell.selectionStyle = .none
cell.layer.cornerRadius = 30
cell.layer.borderColor = UIColor.black.cgColor
cell.layer.borderWidth = 5
cell.layer.cornerCurve = .continuous
return cell
}
//WeatherViewController
//code here for scrollView tableViews
}
//Height for row
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if tableView.tag == 1000{
return view.frame.size.height/7
}
return view.frame.size.height/10
}
//Should Highlight Row
func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
if tableView.tag == 1000{
return true
}
return false
}
//Did select row
func tableView(_ tableView: UITableView, didSelectRowAt
indexPath: IndexPath) {
//calls function for segue to Weather Scroll View (not shown)
if tableView.tag == 1000{
segueToWeatherView(indexPath: indexPath)
}
}
}
EDIT 3: When I comment out another function it finally works, but I'm not sure exactly why, or how to fix it. This is the function in question, addSubViews()
//setup viewController
func addSubViews(){
//add weatherView as subView of ViewController
view.addSubview(weatherView)
//add subviews to weatherView
weatherView.addSubview(scrollView)
weatherView.addSubview(pageControl)
weatherView.addSubview(segueToCityViewButton)
weatherView.addSubview(segueToMapViewButton)
}
Specifically, it works when I comment out this line:
view.addSubview(weatherView)
Here is all the code concerning the setting up of the weatherView and all of its subViews:
//Any additional setup goes here
private func setupViews(){
//VIEWCONTROLLER:
//title
title = "Weather"
//Background color of view Controller
view.backgroundColor = .darkGray
//WEATHERVIEW:
//Background color of weather view Controller
weatherView.backgroundColor = .clear
//weatherView frame
weatherView.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height)
//SCROLLVIEW:
//background color of scroll view
scrollView.backgroundColor = .clear
//scrollView frame
scrollView.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height)
//changed
//PAGECONTROL:
//page control frame
pageControl.frame = CGRect(x: 0, y: view.frame.height-view.frame.size.height/14, width: view.frame.width, height: view.frame.size.height/14)
//TRANSITIONVIEW:
//TransitionView frame
transitionView.frame = CGRect(x: 20, y: 0, width: view.frame.size.width-40, height: view.frame.size.height)
//BUTTONS:
//segue to CityView
segueToCityViewButton.frame = CGRect(x: (weatherView.frame.width/5*4)-20, y: weatherView.frame.height-weatherView.frame.size.height/14, width: weatherView.frame.width/5, height: pageControl.frame.height)
//segue to MapView:
segueToMapViewButton.frame = CGRect(x: 20, y: weatherView.frame.height-weatherView.frame.size.height/14, width: weatherView.frame.width/5, height: pageControl.frame.height)
//LABELS:
transitionViewLabel.frame = transitionView.bounds
//NAVIGATIONBAR:
//set to hidden on initial load
navigationController?.navigationBar.isHidden = true
navigationController?.navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
//NAVIGATION ITEM:
navigationItem.searchController = searchVC
//UISEARCHBARCONTROLLER:
searchVC.searchResultsUpdater = self
}
For the sake of being thorough, here is the full viewDidLoad() Function:
override func viewDidLoad() {
super.viewDidLoad()
//MARK: View Controller
//These two will eventually be moved to the DispatchQueue in APICalls.swift
configureScrollView()
pageControl.numberOfPages = models.count
//Do any setup for the view controller here
setupViews()
//setup ViewController
addSubViews()
//Add Target for the pageControl
addTargetForPageControl()
//MARK: CityViewController
setupCityViewTableViews()
}
EDIT 4: With the following changes in viewDidLoad(), I finally got it to work!
override func viewDidLoad() {
super.viewDidLoad()
//MARK: CityViewController
//Moved to a position before setting up the other views
setupCityViewTableViews()
//MARK: View Controller
//These two will eventually be moved to the DispatchQueue in APICalls.swift
configureScrollView()
pageControl.numberOfPages = models.count
//Do any setup for the view controller here
setupViews()
//setup ViewController
addSubViews()
//Add Target for the pageControl
addTargetForPageControl()
}
Doing it the way you are doing it right now is a way to do it but I think it is the most challenging way to do it for several reasons:
You don't have much control and access to the implementation of the search controller animation within the navigation bar so getting the right coordinates might be a task
Even if you did manage to get the right coordinates, trying to synchronize your animation frames and timing to look in sync and seamless with the search animation on the nav bar will be tricky
I suggest the 2 following alternatives to what you are currently doing where you will get the news experience pretty much for free out of the box.
Option 1: Use a UITableViewController instead of a UIViewController
This is all the code using a UITableViewController and adding a UISearchController to the navigation bar.
class NewsTableViewVC: UITableViewController
{
private let searchController: UISearchController = {
let sc = UISearchController(searchResultsController: nil)
sc.obscuresBackgroundDuringPresentation = false
sc.searchBar.placeholder = "Search"
sc.searchBar.autocapitalizationType = .allCharacters
return sc
}()
override func viewDidLoad()
{
super.viewDidLoad()
view.backgroundColor = .black
title = "Weather"
// Ignore this as you have you own custom cell class
tableView.register(CustomCell.self,
forCellReuseIdentifier: CustomCell.identifier)
setUpNavigationBar()
}
private func setUpNavigationBar()
{
navigationItem.searchController = searchController
}
}
This is the experience you can expect
Option 2: Use auto layouts rather than frames to configure your UITableView
If you don't want to use a UITableViewController, configure your UITableView using auto layout rather than frames which has a little more work but not too much:
class NewsTableViewVC: UIViewController, UITableViewDataSource, UITableViewDelegate
{
private let searchController: UISearchController = {
let sc = UISearchController(searchResultsController: nil)
sc.obscuresBackgroundDuringPresentation = false
sc.searchBar.placeholder = "Search"
sc.searchBar.autocapitalizationType = .allCharacters
return sc
}()
private let tableView = UITableView()
override func viewDidLoad()
{
super.viewDidLoad()
// Just to show it's different from the first
view.backgroundColor = .purple
title = "Weather"
setUpNavigationBar()
setUpTableView()
}
private func setUpNavigationBar()
{
navigationItem.searchController = searchController
}
private func setUpTableView()
{
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.register(CustomCell.self,
forCellReuseIdentifier: CustomCell.identifier)
tableView.dataSource = self
tableView.delegate = self
tableView.backgroundColor = .clear
view.addSubview(tableView)
// Auto Layout
tableView.leadingAnchor
.constraint(equalTo: view.leadingAnchor,
constant: 0).isActive = true
// This important, configure it to the top of the view
// NOT the safe area margins to get the desired result
tableView.topAnchor
.constraint(equalTo: view.topAnchor,
constant: 0).isActive = true
tableView.trailingAnchor
.constraint(equalTo: view.trailingAnchor,
constant: 0).isActive = true
tableView.bottomAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor,
constant: 0).isActive = true
}
}
You can expect the following experience:
Update
This is based on your updated code, you missed one small detail which might be impacting the results you see and this is the top constraint of the UITableView.
You added the constraint to the safeAreaLayoutGuide top anchor:
cityTableView.topAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor,
constant: 0).isActive = true
My recommendation from the code above if you notice is to add it to the view top constraint
// This important, configure it to the top of the view
// NOT the safe area margins to get the desired result
cityTableView.topAnchor
.constraint(equalTo: view.topAnchor,
constant: 0).isActive = true
Give this a go and see if you come close to getting what you expect ?
Here is a link to the complete code of my implementation if it helps:

Can we acces a UIStackView's subview's size?

My question is pretty simple but I haven't found any answer yet.
I am making a sort of table using two vertical stacks inside a horizontal stack. Both vStacks have different objects (Button in my case, with border for each one) but in the same quantity (so that they are horizontally paired like in a classic table).
I have set both of my vStack's distribution to .fillProportionally, and therefore each button have different size depending on their titleLabel length.
However, I would like to make each of my button have the same size of its paired button (the one next to him horizontally, in the other vStack) so that my cells borders would be aligned (using the biggest button's size as a reference in each pair).
I think it involves to find a way to access one stack's subview's size and then constraint the other stack subview to be equally sized. Or, because usually there is only one big button messing with the distribution and offsetting button pairs' border, accessing the way one stack displays its subviews and forcing the other stack to adopt the same way. Either way, I don't know how to do it yet.
I'd be glad if you could help me or lead me to the answer !
(I don't think I need to put code to explain my problem as it's a relatively abstract issue but if you need it I can share it)
EDIT :
Left : What I want, right : What I get
Each cell is a button (useless here but in my app it will have a functionality) with border , I want to set "description" button's height equal as "text" button. I hope it's clearer now :) I tried to invert the layout (two horizontal stacks in one vertical stack) but the issue is still here, with width instead of height this time.
EDIT 2 :
Following your advice, here is some code :
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var bottomButton: UIButton!
#IBOutlet weak var vstack: UIStackView!
#objc func buttonAction(sender: UIButton) {
sender.isEnabled = true
print(sender.frame)
}
override func viewDidLoad() {
super.viewDidLoad()
let button = newButton(text: "name")
let button2 = newButton(text: "John Smith")
let button3 = newButton(text: "Description")
let button4 = newButton(text: "text text text text text text text text text text text \n text text text text text \n text text text text text")
let hStack = UIStackView()
hStack.axis = .horizontal
hStack.distribution = .fillEqually
let hStack2 = UIStackView()
hStack2.axis = .horizontal
hStack2.distribution = .fillEqually
hStack.addArrangedSubview(button)
hStack.addArrangedSubview(button2)
hStack2.addArrangedSubview(button3)
hStack2.addArrangedSubview(button4)
vstack.addArrangedSubview(hStack)
vstack.addArrangedSubview(hStack2)
}
}
func newButton(text: String) -> UIButton {
let button = UIButton(type: .system)
button.isEnabled = true
button.setTitle(text, for: .disabled)
button.setTitle(text, for: .normal)
button.setTitleColor(.black, for: .disabled)
button.layer.borderWidth = 1
button.titleLabel?.numberOfLines = 0
button.titleLabel?.textAlignment = NSTextAlignment.center
return button
}
`
Using horizontal stacks in a vertical stack and Fill Equally partially solves the problem, because it only works when my text is under a certain length, otherwise it clips (see following image), which is why I was using fillProportionally.
OK - part of the problem is that you are modifying the titleLabel properties -- specifically, setting its .numberOfLines = 0. Auto-layout does not take that into account, without a little help.
You'll want to use a button subclass, such as this:
class MultiLineButton: UIButton {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit() -> Void {
titleLabel?.numberOfLines = 0
titleLabel?.textAlignment = .center
// if you want to give your buttons some "padding" around the title
//contentEdgeInsets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0)
}
override var intrinsicContentSize: CGSize {
guard let tl = titleLabel else {
return .zero
}
let size = tl.intrinsicContentSize
return CGSize(width: size.width + contentEdgeInsets.left + contentEdgeInsets.right, height: size.height + contentEdgeInsets.top + contentEdgeInsets.bottom)
}
override func layoutSubviews() {
super.layoutSubviews()
guard let tl = titleLabel else { return }
tl.preferredMaxLayoutWidth = tl.frame.size.width
}
}
Using that class, here is an example view controller where we add a vertical stack view, and 2 horizontal "row" stack views:
class PruViewController: UIViewController {
func newButton(text: String) -> MultiLineButton {
let b = MultiLineButton()
b.titleLabel?.font = .systemFont(ofSize: 15.0)
b.setTitle(text, for: [])
b.setTitleColor(.blue, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
b.setTitleColor(.black, for: .disabled)
b.layer.borderWidth = 1
b.layer.borderColor = UIColor.black.cgColor
return b
}
override func viewDidLoad() {
super.viewDidLoad()
let button = newButton(text: "name")
let button2 = newButton(text: "John Smith")
let button3 = newButton(text: "Description")
let button4 = newButton(text: "text text text text text text text text text text text \n text text text text text \n text text text text text")
let vStack = UIStackView()
vStack.axis = .vertical
vStack.distribution = .fill
let hStack = UIStackView()
hStack.axis = .horizontal
hStack.distribution = .fillEqually
let hStack2 = UIStackView()
hStack2.axis = .horizontal
hStack2.distribution = .fillEqually
hStack.addArrangedSubview(button)
hStack.addArrangedSubview(button2)
hStack2.addArrangedSubview(button3)
hStack2.addArrangedSubview(button4)
vStack.addArrangedSubview(hStack)
vStack.addArrangedSubview(hStack2)
vStack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(vStack)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
vStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
vStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
vStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
}
}
The result:
And, you'll notice in my MultiLineButton class a comment about adding "padding" around the button title labels... here's how it looks with that line un-commented:

UIStackView with multiline label in a UITableViewCell incorrect height

I have a table view where each cell displays an optional title and a multiline subtitle. I want the cell to be self-sizing (i.e grow along with the subtitle but stay as compact as possible). For that I use tableView.rowHeight = UITableView.automaticDimension
The problem I have is that there is quite a lot of vertical spacing around the subtitle in the cells that do have a title.
Cells without title are compressed correctly. Also when I reload the table view, all the layout becomes correct.
Expected behaviour:
Actual behaviour:
The cell has basically a UIStackView pinned to the cell's contentView.
import UIKit
public class TableViewCellSubtitle: UITableViewCell {
private lazy var labelStack: UIStackView = {
let labelStack = UIStackView()
labelStack.alignment = .fill
labelStack.distribution = .fillProportionally
labelStack.axis = .vertical
return labelStack
}()
private lazy var titleLabel: UILabel = {
let titleLabel = UILabel()
titleLabel.backgroundColor = UIColor.blue.withAlphaComponent(0.3)
return titleLabel
}()
private lazy var subtitleLabel: UILabel = {
let subtitleLabel = UILabel()
subtitleLabel.numberOfLines = 0
subtitleLabel.backgroundColor = UIColor.green.withAlphaComponent(0.3)
return subtitleLabel
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
setupConstraints()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
contentView.addSubview(labelStack)
labelStack.translatesAutoresizingMaskIntoConstraints = false
labelStack.addArrangedSubview(titleLabel)
labelStack.addArrangedSubview(subtitleLabel)
}
private func setupConstraints() {
NSLayoutConstraint.activate([
labelStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
labelStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12),
contentView.bottomAnchor.constraint(equalTo: labelStack.bottomAnchor, constant: 12),
contentView.trailingAnchor.constraint(equalTo: labelStack.trailingAnchor, constant: 16)
])
}
public func setup(title: String?, subtitle: String) {
titleLabel.text = title
if title == nil {
labelStack.removeArrangedSubview(titleLabel)
titleLabel.removeFromSuperview()
}
subtitleLabel.text = subtitle
}
}
I tried setting the content hugging on the subtitle
subtitleLabel.setContentHuggingPriority(.required, for: .vertical)
but that makes the title to grow:
If I set it to both:
titleLabel.setContentHuggingPriority(.required, for: .vertical)
subtitleLabel.setContentHuggingPriority(.required, for: .vertical)
it becomes
In all the cases, if I reload the cell or the table view, the layout becomes correct (here on cell tap):
I'm aware that I could layout the cell without using the stack view but my real implementation is a bit more complex
Try with fill instead of fill proportional
Or try to set label.sizeToFit() to collapse label to its content size

Swift: tableview cell change width and postion

I create chatroom
I want user 1 chats position in right and user 2 chats position in left like below image
My code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyChatCell
var frame = cell.frame
let newWidth = frame.width * 0.50
let space = (frame.width - newWidth) / 2
frame.size.width = newWidth
frame.origin.x += space
cell.frame = frame
return cell
}
But my code not work
Please consider using Anchor for your MyChatCellLeft and MyChatCellRight and anchor each example:
cell.messageLabel = "My Message"
cell.floatPosition = true // true left / false right
I must mention I haven't tested this. but you can try it and take it as a blueprint for what you are doing.. you also need to add some kind of logic of who is who... example user1 left user2 right. that depends on your logic...
class ChatBubbleCell: UITableViewCell{
var position: Bool = false
let messageLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
var floatPosition = Bool() {
didSet {
if(floatPosition == true){
position = true
} else {
position = false
}
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews(){
addSubview(messageLabel)
// lef position
if position == true {
let constrains = [messageLabel.topAnchor.constraint(equalTo: topAnchor, constant: 15),
messageLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -15),
messageLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 20)
]
NSLayoutConstraint.activate(constrains)
} else {
let constrains = [messageLabel.topAnchor.constraint(equalTo: topAnchor, constant: 15),
messageLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -15),
messageLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -30)
]
NSLayoutConstraint.activate(constrains)
}
}
}
Consider the cell to be the whole row and use a subview to update frames.
Update the frames on the method of will display cell for row at index path