I've recently started learning swift and iOS app development. I've been doing php backend and low level iOS/macOS programming till now and working with UI is a little hard for me, so please tolerate my stupidity.
If I understand this correctly, stackviews automatically space and contain its subviews in its frame. All the math and layout is done automatically by it. I have a horizontal stackview inside a custom UITableViewCell. The UIStackView is within a UIScrollView because I want the content to be scroll-able. I've set the anchors programmatically (I just can't figure out how to use the storyboard thingies). This is what the cells look like
When I load the view, the stackview doesn't scroll. But it does scroll if I select the cell at least once. The contentSize of the scrollview is set inside the layoutsubviews method of my custom cell.
My Custom Cell
class TableViewCell: UITableViewCell
{
let stackViewLabelContainer = UIStackView()
let scrollViewContainer = UIScrollView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?)
{
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = .black
stackViewLabelContainer.axis = .horizontal
stackViewLabelContainer.distribution = .equalSpacing
stackViewLabelContainer.alignment = .leading
stackViewLabelContainer.spacing = 5
for _ in 1...10
{
let labelView = UILabel();
labelView.backgroundColor = tintColor
labelView.textColor = .white
labelView.text = "ABCD 123"
stackViewLabelContainer.addArrangedSubview(labelView)
}
scrollViewContainer.addSubview(stackViewLabelContainer)
stackViewLabelContainer.translatesAutoresizingMaskIntoConstraints = false
stackViewLabelContainer.leadingAnchor.constraint(equalTo: scrollViewContainer.leadingAnchor).isActive = true
stackViewLabelContainer.topAnchor.constraint(equalTo: scrollViewContainer.topAnchor).isActive = true
addSubview(scrollViewContainer)
scrollViewContainer.translatesAutoresizingMaskIntoConstraints = false
scrollViewContainer.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10).isActive = true
scrollViewContainer.topAnchor.constraint(equalTo: topAnchor, constant: 5).isActive = true
scrollViewContainer.heightAnchor.constraint(equalTo:stackViewLabelContainer.heightAnchor).isActive = true
scrollViewContainer.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
scrollViewContainer.showsHorizontalScrollIndicator = false
}
required init?(coder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews()
{
super.layoutSubviews()
scrollViewContainer.contentSize = CGSize(width: stackViewLabelContainer.frame.width, height: stackViewLabelContainer.frame.height)
}
}
Here's the TableViewController
class TableViewController: UITableViewController {
override func viewDidLoad()
{
super.viewDidLoad()
tableView.register(TableViewCell.self, forCellReuseIdentifier: "reuse_cell")
}
override func numberOfSections(in tableView: UITableView) -> Int
{
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return 5
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "reuse_cell") as! TableViewCell
return cell
}
override func viewDidLayoutSubviews()
{
print("called")
super.viewDidLayoutSubviews()
// let cells = tableView.visibleCells as! Array<TableViewCell>
// cells.forEach
// {
// cell in
// cell.scrollViewContainer.contentSize = CGSize(width: cell.stackViewLabelContainer.frame.width, height: cell.stackViewLabelContainer.frame.height)
//
// }
}
}
I figured out a method to make this work but it affects abstraction and it feels like a weird hack. You get the visible cells from within the UITableViewController, access each scrollview and update its contentSize. There's another fix I found by reversing dyld_shared_cache where I override draw method and stop reusing cells. Both solutions feel like they're far from "proper".
You should constraint the scrollview to the contentView of the cell.
contentView.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.topAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
scrollView.leadingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.leadingAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.trailingAnchor).isActive = true
scrollView.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
Now you can loop your labels and add them as the arranged subviews
for _ in 1...10
{
let labelView = UILabel();
labelView.backgroundColor = tintColor
labelView.textColor = .white
labelView.text = "ABCD 123"
stackView.addArrangedSubview(labelView)
}
Related
I have been trying to scroll the view for a whole day yesterday and I am not able to figure out why it won't scroll. I am not sure what I am doing wrong !!
I have looked at the solutions on stackoverflow:
UIScrollView Scrollable Content Size Ambiguity
How to append a character to a string in Swift?
Right anchor of UIScrollView does not apply
But still, the view doesn't scroll and the scrollview height should be equal to the conrainerView height. But in my case, it stays fixed to the height of the view.
Here is the code repo: https://bitbucket.org/siddharth_shekar/ios_colttestproject/src/master/
Kindly go through and any help would be appreciated. Thanks!!
Here is the code Snippet as well, If you want to go through the constraints and see if there is anything I have added which is not letting the scroll view do its thing !!
I have made changes to the view just one looong text and removed other images, labels, etc to produce the minimal reproducable code.
And I looked at this persons project as well. Their view scrolls!!
https://useyourloaf.com/blog/easier-scrolling-with-layout-guides/
I am just not sure what I am doing differently!!!!
Here is my code for the contentView. It is literally just a textlabel
import Foundation
import UIKit
class RecipeUIView: UIView{
private var recipeTitle: UILabel! = {
let label = UILabel()
label.numberOfLines = 0
label.font = .systemFont(ofSize: 24, weight: .bold)
label.textColor = .gray
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
label.adjustsFontForContentSizeCategory = true
return label
}()
func setupView(currentRecipe: Receipe?){
recipeTitle.text = currentRecipe?.dynamicTitle
addSubview(recipeTitle)
let margin = readableContentGuide
// Constraints
recipeTitle.topAnchor.constraint(equalTo: margin.topAnchor, constant: 4).isActive = true
recipeTitle.leadingAnchor.constraint(equalTo: margin.leadingAnchor, constant: 20).isActive = true
recipeTitle.trailingAnchor.constraint(equalTo: margin.trailingAnchor, constant: -20).isActive = true
}
}
And here is the viewController
import Foundation
import UIKit
class RecipeViewController: UIViewController {
var selectedRecipe: Receipe?
let recipeView: RecipeUIView = {
let view = RecipeUIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
override func viewDidLoad() {
super.viewDidLoad()
recipeView.setupView(currentRecipe: selectedRecipe)
recipeView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)
view.addSubview(scrollView)
scrollView.addSubview(recipeView)
let frameGuide = scrollView.frameLayoutGuide
let contentGuide = scrollView.contentLayoutGuide
// Scroll view layout guides (iOS 11)
NSLayoutConstraint.activate([
frameGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor),
frameGuide.topAnchor.constraint(equalTo: view.topAnchor),
frameGuide.trailingAnchor.constraint(equalTo: view.trailingAnchor),
frameGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor),
contentGuide.leadingAnchor.constraint(equalTo: recipeView.leadingAnchor),
contentGuide.topAnchor.constraint(equalTo: recipeView.topAnchor),
contentGuide.trailingAnchor.constraint(equalTo: recipeView.trailingAnchor),
contentGuide.bottomAnchor.constraint(equalTo: recipeView.bottomAnchor),
contentGuide.widthAnchor.constraint(equalTo: frameGuide.widthAnchor),
])
}
}
And I am still not able to scroll the view. Here is a screenshot of my project output. Still no scroll guide lines on the right!!
UPDATE:: Now the text scrolls, but when I add a UITableView in the UIView the scrolling works but the tableView is not seen in the UiView.
Is it due to the constraints again???
here is the code for the same::
class RecipeUIView: UIView, UITableViewDelegate, UITableViewDataSource{
var currentRecipe: Receipe?
private let tableView: UITableView = {
let tableView = UITableView()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "MyCell")
tableView.backgroundColor = .green
return tableView
}()
private var recipeTitle: UILabel! = {
let label = UILabel()
label.numberOfLines = 0
label.font = .systemFont(ofSize: 24, weight: .bold)
label.textColor = .gray
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
label.adjustsFontForContentSizeCategory = true
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("++++ IngrediantsTableViewCell tableview count: \(currentRecipe?.ingredients.count ?? 0)")
return currentRecipe?.ingredients.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print("++++ IngrediantsTableViewCell tableview cellForRow ")
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath as IndexPath)
cell.textLabel!.text = "\(currentRecipe?.ingredients[indexPath.row].ingredient ?? "")"
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
//return UITableView.automaticDimension
return 30
}
func setupView(currentRecipe: Receipe?){
let margin = readableContentGuide
self.currentRecipe = currentRecipe
recipeTitle.text = currentRecipe?.dynamicTitle
addSubview(recipeTitle)
// Constraints
recipeTitle.topAnchor.constraint(equalTo: margin.topAnchor, constant: 4).isActive = true
recipeTitle.leadingAnchor.constraint(equalTo: margin.leadingAnchor, constant: 20).isActive = true
recipeTitle.trailingAnchor.constraint(equalTo: margin.trailingAnchor, constant: -20).isActive = true
addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: recipeTitle.bottomAnchor, constant: 10).isActive = true
tableView.leadingAnchor.constraint(equalTo: margin.leadingAnchor, constant: 20).isActive = true
tableView.trailingAnchor.constraint(equalTo: margin.trailingAnchor, constant: -20).isActive = true
tableView.bottomAnchor.constraint(equalTo: margin.bottomAnchor, constant: -20).isActive = true
tableView.reloadData()
}
}
You are missing a constraint...
In your RecipeUIView class, you have this:
func setupView(currentRecipe: Receipe?){
recipeTitle.text = currentRecipe?.dynamicTitle
addSubview(recipeTitle)
let margin = readableContentGuide
// Constraints
recipeTitle.topAnchor.constraint(equalTo: margin.topAnchor, constant: 4).isActive = true
recipeTitle.leadingAnchor.constraint(equalTo: margin.leadingAnchor, constant: 20).isActive = true
recipeTitle.trailingAnchor.constraint(equalTo: margin.trailingAnchor, constant: -20).isActive = true
}
So, you have no constraint controlling the view's Height.
Add this line:
recipeTitle.bottomAnchor.constraint(equalTo: margin.bottomAnchor, constant: -20).isActive = true
And you'll get vertical scrolling.
Two side notes...
First, in ``RecipeViewController`, change your constraints like this:
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
recipeView.leadingAnchor.constraint(equalTo: contentGuide.leadingAnchor),
recipeView.topAnchor.constraint(equalTo: contentGuide.topAnchor),
recipeView.trailingAnchor.constraint(equalTo: contentGuide.trailingAnchor),
recipeView.bottomAnchor.constraint(equalTo: contentGuide.bottomAnchor),
recipeView.widthAnchor.constraint(equalTo: frameGuide.widthAnchor),
])
There's no real functional difference, but it is more logical and more readable to think in terms of:
I'm constraining the scrollView to the view
I'm constraining the recipeView to the scroll view's .contentLayoutGuide (which determines the "scrollable" size)
I'm constraining the recipeView width the the scroll view's .frameLayoutGuide
Second, giving views contrasting background colors can be very helpful when trying to debug layouts.
For example, if I set background colors like this:
recipeTitle label : cyan
recipeView : yellow
scrollView : orange
It looks like this when running (with your original constraints):
Since the cyan label is a subview of the yellow view, it is obvious that the yellow view height is not correct.
After add the missing bottom constraint, it looks like this:
I'm trying to figure out is it possible for UICollectionView to calculate it's own height using autolayout? My custom cells are built on autolayout and the UICollectionViewFlowLayout.automaticSize property for itemSize seems to be working, but the size of UICollectionView itself should be set. I believe that this is normal behavior, since the collection can have bigger size than it's cells, but maybe it is possible to make height of the content view of UICollectionView to be equal to cell with some insets?
Here is the code for test UIViewController with UICollectionView
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
var collectionView: UICollectionView?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = CGSize(width: UIScreen.main.bounds.width, height: 100)
layout.itemSize = UICollectionViewFlowLayout.automaticSize
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
if let collectionView = collectionView {
collectionView.showsHorizontalScrollIndicator = false
collectionView.isPagingEnabled = true
collectionView.register(CollectionViewCell.self,
forCellWithReuseIdentifier: "CollectionViewCell")
collectionView.backgroundColor = .clear
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
collectionView.delegate = self
collectionView.dataSource = self
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
collectionView.leftAnchor.constraint(equalTo: view.leftAnchor),
collectionView.rightAnchor.constraint(equalTo: view.rightAnchor),
// I want to get rid of this constraint
collectionView.heightAnchor.constraint(equalToConstant: 200)
])
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
5
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell",
for: indexPath) as? CollectionViewCell else {
return UICollectionViewCell()
}
return cell
}
}
And for custom cell
final class CollectionViewCell: UICollectionViewCell {
var mainView = UIView()
var bigView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .yellow
return view
}()
var label: UILabel = {
let label = UILabel()
label.text = "Here is text"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
var anotherLabel: UILabel = {
let label = UILabel()
label.text = "Here may be no text"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let inset: CGFloat = 16.0
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(mainView)
mainView.translatesAutoresizingMaskIntoConstraints = false
mainView.backgroundColor = .white
addSubview(bigView)
addSubview(label)
addSubview(anotherLabel)
NSLayoutConstraint.activate([
mainView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width),
mainView.topAnchor.constraint(equalTo: topAnchor),
mainView.leftAnchor.constraint(equalTo: leftAnchor),
mainView.rightAnchor.constraint(equalTo: rightAnchor),
mainView.bottomAnchor.constraint(equalTo: bottomAnchor),
bigView.topAnchor.constraint(equalTo: mainView.topAnchor, constant: inset),
bigView.leftAnchor.constraint(equalTo: mainView.leftAnchor, constant: inset),
bigView.rightAnchor.constraint(equalTo: mainView.rightAnchor, constant: -inset),
bigView.bottomAnchor.constraint(equalTo: mainView.bottomAnchor, constant: -inset),
label.topAnchor.constraint(equalTo: bigView.topAnchor, constant: inset),
label.leftAnchor.constraint(equalTo: bigView.leftAnchor, constant: inset),
label.rightAnchor.constraint(equalTo: bigView.rightAnchor, constant: -inset),
anotherLabel.topAnchor.constraint(equalTo: label.bottomAnchor, constant: inset),
anotherLabel.leftAnchor.constraint(equalTo: bigView.leftAnchor, constant: inset),
anotherLabel.rightAnchor.constraint(equalTo: bigView.rightAnchor, constant: -inset),
anotherLabel.bottomAnchor.constraint(equalTo: bigView.bottomAnchor, constant: -inset)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
And maybe someone, who knows the answer also could tell whether it's possible to later put this paging collection into UITalbleViewCell and make size of the cell change with selected item in the collection? I provide screenshot of the collection I'm trying to make:
Item's without second text label will have smaller height, than items on the picture
Screenshot
Solution 1:
Get collection view content size from collectionViewLayout :
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// Set your collection view height here.
let collectionHeight = self.yourCollectionView.collectionViewLayout.collectionViewContentSize.height
}
Solution 2:
Use KVO.
// Register observer
self.yourCollectionView.addObserver(self, forKeyPath: "contentSize", options: [.new, .old, .prior], context: nil)
#objc override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "contentSize" {
// content size changed. Set your collection view height here.
let contentSize = change?[NSKeyValueChangeKey.newKey] as? CGSize {
print("contentSize:", contentSize)
}
}
// Remove register observer
deinit {
self.yourCollectionView.removeObserver(self, forKeyPath: "contentSize")
}
Solution 3:
Assign this class to UICollectionView. If you set height constraint from storyboard then set and enable remove at runtime.
class DynamicCollectionView: UICollectionView {
override func layoutSubviews() {
super.layoutSubviews()
if !__CGSizeEqualToSize(bounds.size, self.intrinsicContentSize) {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
var size = contentSize
size.height += (contentInset.top + contentInset.bottom)
size.width += (contentInset.left + contentInset.right)
return size
}
}
I'm creating a simple app using Swift 5, Xcode 11, and a ui table view controller. Inside the ui table view, I want 2 buttons: One button on the left of my table view, the other on the right. I have tried many other related/similar question's answers, but all of them failed(probably because 1. Too Old, 2. Answer written in OBJ-C).
Here's my Table View Controller:
import UIKit
#objcMembers class CustomViewController: UITableViewController {
var tag = 0
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
}
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
// 3
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
tag = tag + 1
let cell = tableView.dequeueReusableCell(withIdentifier: "themeCell", for: indexPath) as! ThemeCell
var cellButton: UIButton!
cellButton = UIButton(frame: CGRect(x: 5, y: 5, width: 50, height: 30))
cell.addSubview(cellButton)
cell.img.image = UIImage(named: SingletonViewController.themes[indexPath.row])
cell.accessoryView = cellButton
cellButton.backgroundColor = UIColor.red
cellButton.tag = tag
return cell
}
}
Here's what I'm currently getting:
add below code for apply constraint worked
let cellButton = UIButton(frame: CGRect.zero)
cellButton.translatesAutoresizingMaskIntoConstraints = false
cell.addSubview(cellButton)
cell.accessoryView = cellButton
cellButton.backgroundColor = UIColor.red
cellButton.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 5).isActive = true
cellButton.topAnchor.constraint(equalTo: cell.topAnchor, constant: 5).isActive = true
cellButton.widthAnchor.constraint(equalToConstant: 50).isActive = true
cellButton.heightAnchor.constraint(equalToConstant: 30).isActive = true
You can achieve right and left align button from constraints.
Below is my code to align view right or left.
override func viewDidLoad() {
let leftButton = UIButton(type: .custom)
leftButton.backgroundColor = UIColor.red
self.view.addSubview(leftButton)
leftButton.translatesAutoresizingMaskIntoConstraints = false
let horizontalConstraint = leftButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20)
let verticalConstraint = leftButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
let widthConstraint = leftButton.widthAnchor.constraint(equalToConstant: 100)
let heightConstraint = leftButton.heightAnchor.constraint(equalToConstant: 100)
NSLayoutConstraint.activate([horizontalConstraint, verticalConstraint, widthConstraint, heightConstraint])
let rightButton = UIButton(type: .custom)
rightButton.backgroundColor = UIColor.red
self.view.addSubview(rightButton)
rightButton.translatesAutoresizingMaskIntoConstraints = false
let horizontalConstraintRight = rightButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
let verticalConstraintRight = rightButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
let widthConstraintRight = rightButton.widthAnchor.constraint(equalToConstant: 100)
let heightConstraintRight = rightButton.heightAnchor.constraint(equalToConstant: 100)
NSLayoutConstraint.activate([horizontalConstraintRight, verticalConstraintRight, widthConstraintRight, heightConstraintRight])
}
[![Left and Right Aligned View Constraints][1]][1]
[1]: https://i.stack.imgur.com/tMJ8N.jpg
As the cell is getting reuse, you have to put buttons in your xib file. You should not make a button every time and add it in a cell. Try this by adding a button in xib.
class ThemeCell: UITableViewCell {
//MARK:- Initialize View
private let button : UIButton = {
let button = UIButton()
button.setTitle("Hello", .normal)
button.backgroundColor = .red
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
//MARK:- View Life Cycle
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setup()
}
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
selectionStyle = .none
}
//MARK:- User Defined Function
private func setup() {
addSubView(button)
button.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20).isActive = true
button.topAnchor.constraint(equalTo: topAnchor, constant: 20).isActive = true
button.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20).isActive = true
}
}
You can use AutoLayout Constraints like this to setup the button. No need to call it in the cellForRow method.
I am adding a UITableView into vertical UIStackView. That vertical UIStackView is within a UIScrollView.
However the table is not displaying unless I force an explicit Height constraint on it which I obviously don't want to do.
According to this SO question and answer UITableView not shown inside UIStackView this is because "stackview tries to compress content as much as possible"
If I add a UILabel to the StackView it is displayed fine. There is something specific about the UITableView that means it is not. I am using Xamarin and creating the UITableView in code
this.recentlyOpenedPatientsTable = new UITableView()
{
RowHeight = UITableView.AutomaticDimension,
EstimatedRowHeight = 44.0f,
AllowsMultipleSelectionDuringEditing = false,
TranslatesAutoresizingMaskIntoConstraints = false,
Editing = false,
BackgroundColor = UIColor.Clear,
TableFooterView = new UIView(),
ScrollEnabled = false,
};
The UIScrollView is pinned to the Top, Bottom, Left and Right of the View and works fine. It takes the Height I expect.
I have tried both the suggestions in this SO question and neither have worked. I find it odd that I cannot find others having this issue.
Any other suggestions?
Here is a very basic example, using a UITableView subclass to make it auto-size its height based on its content.
The red buttons (in a horizontal stack view) are the first arranged subView in the vertical stack view.
The table is next (green background for the cells' contentView, yellow background for a multi-line label).
And the last arranged subView is a cyan background UILabel:
Note that the vertical stack view is constrained 40-pts from Top, Leading and Trailing, and at least 40-pts from the Bottom. If you add enough rows to the table to exceed the available height, you'll have to scroll to see the additional rows.
//
// TableInStackViewController.swift
//
// Created by Don Mag on 6/24/19.
//
import UIKit
final class ContentSizedTableView: UITableView {
override var contentSize:CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
class TableInStackCell: UITableViewCell {
let theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .yellow
v.textAlignment = .left
v.numberOfLines = 0
return v
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.backgroundColor = .green
contentView.addSubview(theLabel)
NSLayoutConstraint.activate([
theLabel.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor, constant: 0.0),
theLabel.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor, constant: 0.0),
theLabel.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor, constant: 0.0),
theLabel.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor, constant: 0.0),
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class TableInStackViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let theStackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.alignment = .fill
v.distribution = .fill
v.spacing = 8
return v
}()
let addButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Add a Row", for: .normal)
v.backgroundColor = .red
return v
}()
let deleteButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Delete a Row", for: .normal)
v.backgroundColor = .red
return v
}()
let buttonsStack: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.alignment = .fill
v.distribution = .fillEqually
v.spacing = 20
return v
}()
let theTable: ContentSizedTableView = {
let v = ContentSizedTableView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let bottomLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
v.textAlignment = .center
v.numberOfLines = 0
v.text = "This label is the last element in the stack view."
// prevent label from being compressed when the table gets too tall
v.setContentCompressionResistancePriority(.required, for: .vertical)
return v
}()
var theTableData: [String] = [
"Content Sized Table View",
"This row shows that the cell heights will auto-size, based on the cell content (multi-line label in this case).",
"Here is the 3rd default row",
]
var minRows = 1
let reuseID = "TableInStackCell"
override func viewDidLoad() {
super.viewDidLoad()
minRows = theTableData.count
view.addSubview(theStackView)
NSLayoutConstraint.activate([
// constrain stack view 40-pts from top, leading and trailing
theStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40.0),
theStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 40.0),
theStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -40.0),
// constrain stack view *at least* 40-pts from bottom
theStackView.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40.0),
])
buttonsStack.addArrangedSubview(addButton)
buttonsStack.addArrangedSubview(deleteButton)
theStackView.addArrangedSubview(buttonsStack)
theStackView.addArrangedSubview(theTable)
theStackView.addArrangedSubview(bottomLabel)
theTable.delegate = self
theTable.dataSource = self
theTable.register(TableInStackCell.self, forCellReuseIdentifier: reuseID)
addButton.addTarget(self, action: #selector(addRow), for: .touchUpInside)
deleteButton.addTarget(self, action: #selector(deleteRow), for: .touchUpInside)
}
#objc func addRow() -> Void {
// add a row to our data source
let n = theTableData.count - minRows
theTableData.append("Added Row: \(n + 1)")
theTable.reloadData()
}
#objc func deleteRow() -> Void {
// delete a row from our data source (keeping the original rows intact)
let n = theTableData.count
if n > minRows {
theTableData.remove(at: n - 1)
theTable.reloadData()
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return theTableData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseID, for: indexPath) as! TableInStackCell
cell.theLabel.text = theTableData[indexPath.row]
return cell
}
}
I'm following (I did make some minor changes in how I do the constraints of the tableViewCell, I'm using a stackView instead) this RayWenderlich tutorial. It's about dynamic self sizing tableViewcells. My problem is that my tableView initially loads with 3 rows (on iPhone 6s) or starts with 2 rows on iPhone 7 Plus. FYI when I debug, my dataSource's count shows that it's 4 rows. If I scroll back and forth a few times then maybe one other row would show up, or maybe both or maybe one would show but one other would just vanish!
In the gif below it loads the screen initially with 3 rows, then when I scroll it shows only 2 rows then again it shows 3 rows, but the 3rd is different this time. It should always be showing 4 rows!
My Models are a Work struct
struct Work {
let title: String
let image: UIImage
let info: String
var isExpanded: Bool
}
and an Artist struct:
struct Artist {
let name: String
let bio: String
let image: UIImage
var works: [Work]
init(name: String, bio: String, image: UIImage, works: [Work]) {
self.name = name
self.bio = bio
self.image = image
self.works = works
}
}
My WorkTableViewCell is as below:
import UIKit
class WorkTableViewCell: UITableViewCell {
static let textViewText = "select for more info >"
var work : Work! {
didSet{
titleLabel.text = work.title
workImageView.image = work.image
}
}
lazy var titleLabel : UILabel = {
let label = UILabel()
label.backgroundColor = .cyan
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
lazy var workImageView : UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
// imageView.setContentHuggingPriority(1000, for: .vertical)
// imageView.setContentHuggingPriority(2, for: .vertical)
return imageView
}()
lazy var moreInfoTextView : UITextView = {
let textView = UITextView()
textView.translatesAutoresizingMaskIntoConstraints = false
textView.textAlignment = .center
textView.isScrollEnabled = false
return textView
}()
//
// override func prepareForReuse() {
// super.prepareForReuse()
//
// imageView?.image = nil
//
// }
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
addConstraints()
}
private func addConstraints(){
let stackView = UIStackView(arrangedSubviews: [workImageView, titleLabel, moreInfoTextView])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.distribution = .fillProportionally
stackView.alignment = .center
contentView.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
stackView.topAnchor.constraint(equalTo: contentView.topAnchor),
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func awakeFromNib() {
super.awakeFromNib()
}
}
And this is the cellForRowAt method of my tableView:
extension ArtistDetailViewController: UITableViewDataSource{
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! WorkTableViewCell
cell.work = works[indexPath.row]
cell.moreInfoTextView.text = works[indexPath.row].isExpanded ? works[indexPath.row].info : WorkTableViewCell.textViewText
cell.moreInfoTextView.textAlignment = works[indexPath.row].isExpanded ? .left : .center
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return works.count
}
}
I also did add tableView.reloadData() inside the viewWillAppear and viewDidAppear but nothing changed.
So changing the distribution of the stackView was the fix.
I just had to change:
stackView.distribution = .fillProportionally
to:
stackView.distribution = .fill
I'm not sure I understand why that's necessary though.