Multi-line UILabel in TableView header [duplicate] - ios

I am currently using a UIViewController and adding a UITableView to the view. With this tableView I am adding a UIView called containerView to its tableHeaderView. I set the height of the container view and then adding a second UIView to its subview, that is pinned to the bottom of the containerView.
When I add it to the header view the cells are being overlapped. What's odd though is that if I don't add the subview to the container view the headerView is not being overlapped by the cells, it is only occurring when I am adding the second view as a subview to the container view.
class ViewController: UIViewController {
private var containerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.alpha = 0.7
view.backgroundColor = .red
return view
}()
private var bottomView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .blue
return view
}()
private(set) lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
containerView.addSubview(bottomView)
tableView.tableHeaderView = containerView
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
containerView.topAnchor.constraint(equalTo: tableView.topAnchor),
containerView.heightAnchor.constraint(equalToConstant: 214),
containerView.widthAnchor.constraint(equalToConstant: view.frame.size.width),
bottomView.topAnchor.constraint(equalTo: containerView.bottomAnchor),
bottomView.heightAnchor.constraint(equalToConstant: 114),
bottomView.widthAnchor.constraint(equalToConstant: view.frame.size.width),
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.contentInset = UIEdgeInsets(top: -view.safeAreaInsets.top, left: 0, bottom: 0, right: 0)
tableView.tableHeaderView?.autoresizingMask = []
tableView.tableHeaderView?.layoutIfNeeded()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
}
}

The reason your "blue view" is overlapping the cells is because you are constraining its Top to the red view's Bottom, but you're not updating the header view size.
One good approach is to create a UIView subclass to use as your header view. Setup all of its content with proper auto-layout constraints.
Then, in the controller's viewDidLayoutSubviews(), we use .systemLayoutSizeFitting(...) to determine the header view's height and update its frame:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// update table header size
guard let headerView = tableView.tableHeaderView else { return }
let height = headerView.systemLayoutSizeFitting(CGSize(width: tableView.frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow).height
var frame = headerView.frame
// avoids infinite loop!
if height != frame.height {
frame.size.height = height
headerView.frame = frame
tableView.tableHeaderView = headerView
}
}
Here is a complete example...
First, our custom view class:
class SampleHeaderView: UIView {
let redView: UIView = {
let v = UIView()
v.backgroundColor = .systemRed
return v
}()
let blueView: UIView = {
let v = UIView()
v.backgroundColor = .systemBlue
return v
}()
let redTopLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .yellow
v.numberOfLines = 0
return v
}()
let redBottomLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .green
v.numberOfLines = 0
return v
}()
let multiLineLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .cyan
v.numberOfLines = 0
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// all views will use auto-layout
[redView, blueView, redTopLabel, redBottomLabel, multiLineLabel].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
// prevent label vertical compression
[redTopLabel, redBottomLabel, multiLineLabel].forEach { v in
v.setContentCompressionResistancePriority(.required, for: .vertical)
}
// add top and bottom labels to red view
redView.addSubview(redTopLabel)
redView.addSubview(redBottomLabel)
// add multi-line label to blue view
blueView.addSubview(multiLineLabel)
// add red and blue views to self
addSubview(redView)
addSubview(blueView)
// the following constraints need to have less-than required to avoid
// auto-layout warnings
// blue view bottom to self
let c1 = blueView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0)
// labels trailing contraints
let c2 = redTopLabel.trailingAnchor.constraint(equalTo: redView.trailingAnchor, constant: -8.0)
let c3 = redBottomLabel.trailingAnchor.constraint(equalTo: redView.trailingAnchor, constant: -8.0)
let c4 = multiLineLabel.trailingAnchor.constraint(equalTo: blueView.trailingAnchor, constant: -8.0)
[c1, c2, c3, c4].forEach { c in
c.priority = .required - 1
}
NSLayoutConstraint.activate([
// red view top to self
redView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
// leading / trailing to self
redView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
redView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
// blue view top to red view bottom
blueView.topAnchor.constraint(equalTo: redView.bottomAnchor, constant: 0.0),
// leading / trailing to self
blueView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
blueView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
// top and bottom labels, constrained in red view
// with a little "padding"
redTopLabel.topAnchor.constraint(equalTo: redView.topAnchor, constant: 8.0),
redTopLabel.leadingAnchor.constraint(equalTo: redView.leadingAnchor, constant: 8.0),
redBottomLabel.topAnchor.constraint(equalTo: redTopLabel.bottomAnchor, constant: 8.0),
redBottomLabel.leadingAnchor.constraint(equalTo: redView.leadingAnchor, constant: 8.0),
redBottomLabel.bottomAnchor.constraint(equalTo: redView.bottomAnchor, constant: -8.0),
// multi-line label, constrained in blue view
// with a little "padding"
multiLineLabel.topAnchor.constraint(equalTo: blueView.topAnchor, constant: 8.0),
multiLineLabel.leadingAnchor.constraint(equalTo: blueView.leadingAnchor, constant: 8.0),
multiLineLabel.bottomAnchor.constraint(equalTo: blueView.bottomAnchor, constant: -8.0),
// the less-than-required priority constraints
c1, c2, c3, c4,
])
}
}
and a sample controller:
class TableHeaderViewController: UIViewController {
var sampleHeaderView = SampleHeaderView()
private(set) lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
sampleHeaderView.redTopLabel.text = "The Red Top Label"
sampleHeaderView.redBottomLabel.text = "The Red Bottom Label, with enough text that is should wrap."
sampleHeaderView.multiLineLabel.text = "This text is for the Label in the Blue View. It is also long enough that it will require word-wrapping. Note that the header updates itself when the frame changes, such as on device rotation."
tableView.tableHeaderView = sampleHeaderView
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// update table header size
guard let headerView = tableView.tableHeaderView else { return }
let height = headerView.systemLayoutSizeFitting(CGSize(width: tableView.frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow).height
var frame = headerView.frame
// avoids infinite loop!
if height != frame.height {
frame.size.height = height
headerView.frame = frame
tableView.tableHeaderView = headerView
}
}
}
extension TableHeaderViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 20
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
c.textLabel?.text = "\(indexPath)"
return c
}
}
Output:
and rotated:

Related

Rendering all the cells of UITableview at once to generate tableview screenshot

I have a view controller with the below UI layout.
There is a header view at the top with 3 labels, a footer view with 2 buttons at the bottom and an uitableview inbetween header view and footer view. The uitableview is dynamically loaded and on average has about 6 tableview cells. One of the buttons in the footer view is take screenshot button where i need to take the screenshot of full tableview. In small devices like iPhone 6, the height of the table is obviously small as it occupies the space between header view and footer view. So only 4 cells are visible to the user and as the user scrolls others cells are loaded into view. If the user taps take screen shot button without scrolling the table view, the last 2 cells are not captured in the screenshot. The current implementation tried to negate this by changing table view frame to table view content size before capturing screenshot and resetting frame after taking screenshot, but this approach is not working starting iOS 13 as the table view content size returns incorrect values.
Current UI layout implementation
Our first solution is to embed the tableview inside the scrollview and have the tableview's scroll disabled. By this way the tableview will be forced to render all cells at once. We used the below custom table view class to override intrinsicContentSize to make the tableview adjust itself to correct height based on it contents
class CMDynamicHeightAdjustedTableView: UITableView {
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
return self.contentSize
}
override var contentSize: CGSize {
didSet {
self.invalidateIntrinsicContentSize()
}
}
override func reloadData() {
super.reloadData()
self.invalidateIntrinsicContentSize()
}
}
Proposed UI implementation
But we are little worried about how overriding intrinsicContentSize could affect performance and other apple's internal implementations
So our second solution is to set a default initial height constraint for tableview and observe the tableview's content size keypath and update the tableview height constraint accordingly. But the content size observer gets called atleast 12-14 times before the screen elements are visible to the user.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.confirmationTableView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "contentSize" {
if object is UITableView {
if let newvalue = change?[.newKey], let newSize = newvalue as? CGSize {
self.confirmationTableViewHeightConstraint.constant = newSize.height
}
}
}
}
Will the second approach impact performance too?
What is the better approach of the two?
Is there any alternate solution?
I am not sure, but if I understood correctly when you screenshot the TableView the last 2 cells are not loaded because of the tableview being between the Header and Footer. Here are two options I would consider:
Option 1
Try to make the TableView frame start from the Header and have the height of the Unscreen.main.bounds.height - the Header view frame. This would mean that the tableView will expand toward the end of the screen. Then add the Footer over the tableView in the desired relation.
Option 2
Try before screenshooting, to reloadRows at two level below the current Level. You can get the current indexPath of the UITableView, when the TableView reloads it from its delegate, store it somewhere always the last indexPath used, and when screenshot reload the two below.
You can "temporarily" change the height of your table view, force it to update, render it to a UIImage, and then set the height back.
Assuming you have your "Header" view constrained to the top, your "Footer" view constrained to the bottom, and your table view constrained between them...
Add a class var/property for the table view's bottom constraint:
var tableBottomConstraint: NSLayoutConstraint!
then set that constraint:
tableBottomConstraint = tableView.bottomAnchor.constraint(equalTo: footerView.topAnchor, constant: 0.0)
When you want to "capture" the table:
func captureTableView() -> UIImage {
// save the table view's bottom constraint's constant
// and the contentOffset y position
let curConstant = tableBottomConstraint.constant
let curOffset = tableView.contentOffset.y
// make table view really tall, to guarantee all rows will fit
tableBottomConstraint.constant = 20000
// force it to update
tableView.setNeedsLayout()
tableView.layoutIfNeeded()
UIGraphicsBeginImageContextWithOptions(tableView.contentSize, false, UIScreen.main.scale)
tableView.layer.render(in: UIGraphicsGetCurrentContext()!)
// get the image
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext();
// set table view state back to what it was
tableBottomConstraint.constant = curConstant
tableView.contentOffset.y = curOffset
return image
}
Here is a complete example you can run to test it:
class SimpleCell: UITableViewCell {
let theLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 0
v.backgroundColor = .yellow
return v
}()
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() {
theLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(theLabel)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
theLabel.topAnchor.constraint(equalTo: g.topAnchor),
theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
theLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
theLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
}
}
class TableCapVC: UIViewController, UITableViewDataSource, UITableViewDelegate {
let tableView = UITableView()
// let's use 12 rows, each with 1, 2, 3 or 4 lines of text
// so it will definitely be too many rows to see on the screen
let numRows: Int = 12
var tableBottomConstraint: NSLayoutConstraint!
// we'll use this to display that captured table view image
let resultHolder = UIView()
let resultImageView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let headerView = myHeaderView()
let footerView = myFooterView()
[headerView, tableView, footerView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
// we will use this to change the bottom constraint of the table view
// when we want to capture it
tableBottomConstraint = tableView.bottomAnchor.constraint(equalTo: footerView.topAnchor, constant: 0.0)
NSLayoutConstraint.activate([
// constrain "header" view at the top
headerView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
headerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
headerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
// constrain "fotter" view at the bottom
footerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
footerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
footerView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
// constrain table view between header and footer views
tableView.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 0.0),
tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
tableBottomConstraint,
])
tableView.register(SimpleCell.self, forCellReuseIdentifier: "c")
tableView.dataSource = self
tableView.delegate = self
// we'll add a UIImageView (in a "holder" view) on top of the table
// then show/hide it to see the results of
// the table capture
resultImageView.backgroundColor = .gray
resultImageView.layer.borderColor = UIColor.cyan.cgColor
resultImageView.layer.borderWidth = 1
resultImageView.layer.cornerRadius = 16.0
resultImageView.layer.shadowColor = UIColor.black.cgColor
resultImageView.layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
resultImageView.layer.shadowRadius = 8
resultImageView.layer.shadowOpacity = 0.9
resultImageView.contentMode = .scaleAspectFit
resultHolder.alpha = 0.0
resultHolder.translatesAutoresizingMaskIntoConstraints = false
resultImageView.translatesAutoresizingMaskIntoConstraints = false
resultHolder.addSubview(resultImageView)
view.addSubview(resultHolder)
NSLayoutConstraint.activate([
// cover everything with the clear "holder" view
resultHolder.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
resultHolder.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
resultHolder.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
resultHolder.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
resultImageView.topAnchor.constraint(equalTo: resultHolder.topAnchor, constant: 20.0),
resultImageView.leadingAnchor.constraint(equalTo: resultHolder.leadingAnchor, constant: 20.0),
resultImageView.trailingAnchor.constraint(equalTo: resultHolder.trailingAnchor, constant: -20.0),
resultImageView.bottomAnchor.constraint(equalTo: resultHolder.bottomAnchor, constant: -20.0),
])
// tap image view / holder view when showing to hide it
let t = UITapGestureRecognizer(target: self, action: #selector(hideImage))
resultHolder.addGestureRecognizer(t)
}
func myHeaderView() -> UIView {
let v = UIView()
v.backgroundColor = .systemBlue
let sv = UIStackView()
sv.axis = .vertical
sv.spacing = 4
let strs: [String] = [
"\"Header\" and \"Footer\" views",
"are separate views - they are not",
".tableHeaderView / .tableFooterView",
]
strs.forEach { str in
let label = UILabel()
label.text = str
label.textAlignment = .center
label.font = .systemFont(ofSize: 13.0, weight: .regular)
label.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
sv.addArrangedSubview(label)
}
sv.translatesAutoresizingMaskIntoConstraints = false
v.addSubview(sv)
NSLayoutConstraint.activate([
sv.topAnchor.constraint(equalTo: v.topAnchor, constant: 8.0),
sv.leadingAnchor.constraint(equalTo: v.leadingAnchor, constant: 8.0),
sv.trailingAnchor.constraint(equalTo: v.trailingAnchor, constant: -8.0),
sv.bottomAnchor.constraint(equalTo: v.bottomAnchor, constant: -8.0),
])
return v
}
func myFooterView() -> UIView {
let v = UIView()
v.backgroundColor = .systemPink
let sv = UIStackView()
sv.axis = .horizontal
sv.spacing = 12
sv.distribution = .fillEqually
let btn1: UIButton = {
var cfg = UIButton.Configuration.filled()
cfg.title = "Capture Table"
let b = UIButton(configuration: cfg)
b.addTarget(self, action: #selector(btn1Action(_:)), for: .touchUpInside)
return b
}()
let btn2: UIButton = {
var cfg = UIButton.Configuration.filled()
cfg.title = "Another Button"
let b = UIButton(configuration: cfg)
b.addTarget(self, action: #selector(btn2Action(_:)), for: .touchUpInside)
return b
}()
sv.addArrangedSubview(btn1)
sv.addArrangedSubview(btn2)
sv.translatesAutoresizingMaskIntoConstraints = false
v.addSubview(sv)
NSLayoutConstraint.activate([
sv.topAnchor.constraint(equalTo: v.topAnchor, constant: 8.0),
sv.leadingAnchor.constraint(equalTo: v.leadingAnchor, constant: 8.0),
sv.trailingAnchor.constraint(equalTo: v.trailingAnchor, constant: -8.0),
sv.bottomAnchor.constraint(equalTo: v.bottomAnchor, constant: -8.0),
])
return v
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return numRows
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath) as! SimpleCell
let nLines = indexPath.row % 4
var s: String = "Row: \(indexPath.row)"
for i in 0..<nLines {
s += "\nLine \(i+2)"
}
c.theLabel.text = s
return c
}
#objc func btn1Action(_ sender: UIButton) {
let img = captureTableView()
print("TableView Image Captured - size:", img.size)
// do something with the tableView capture
// maybe save it to documents folder?
// for this example, we will show it
resultImageView.image = img
UIView.animate(withDuration: 0.5, animations: {
self.resultHolder.alpha = 1.0
})
}
#objc func hideImage() {
UIView.animate(withDuration: 0.5, animations: {
self.resultHolder.alpha = 0.0
})
}
#objc func btn2Action(_ sender: UIButton) {
print("Another Button Tapped")
}
func captureTableView() -> UIImage {
// save the table view's bottom constraint's constant
// and the contentOffset y position
let curConstant = tableBottomConstraint.constant
let curOffset = tableView.contentOffset.y
// make table view really tall, to guarantee all rows will fit
tableBottomConstraint.constant = 20000
// force it to update
tableView.setNeedsLayout()
tableView.layoutIfNeeded()
UIGraphicsBeginImageContextWithOptions(tableView.contentSize, false, UIScreen.main.scale)
tableView.layer.render(in: UIGraphicsGetCurrentContext()!)
// get the image
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext();
// set table view state back to what it was
tableBottomConstraint.constant = curConstant
tableView.contentOffset.y = curOffset
return image
}
}
We give the table 12 rows, each with 1, 2, 3 or 4 lines of text so it will definitely be too many rows to see on the screen. Tapping on the "Capture Table" button will capture the table to a UIImage and then display that image. Tap on the image to dismiss it:

UIScrollVIew not Scrolling Vertically programatically no matter what

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:

tableHeaderView is overlapping cells when adding custom view to subview of container view

I am currently using a UIViewController and adding a UITableView to the view. With this tableView I am adding a UIView called containerView to its tableHeaderView. I set the height of the container view and then adding a second UIView to its subview, that is pinned to the bottom of the containerView.
When I add it to the header view the cells are being overlapped. What's odd though is that if I don't add the subview to the container view the headerView is not being overlapped by the cells, it is only occurring when I am adding the second view as a subview to the container view.
class ViewController: UIViewController {
private var containerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.alpha = 0.7
view.backgroundColor = .red
return view
}()
private var bottomView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .blue
return view
}()
private(set) lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
containerView.addSubview(bottomView)
tableView.tableHeaderView = containerView
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
containerView.topAnchor.constraint(equalTo: tableView.topAnchor),
containerView.heightAnchor.constraint(equalToConstant: 214),
containerView.widthAnchor.constraint(equalToConstant: view.frame.size.width),
bottomView.topAnchor.constraint(equalTo: containerView.bottomAnchor),
bottomView.heightAnchor.constraint(equalToConstant: 114),
bottomView.widthAnchor.constraint(equalToConstant: view.frame.size.width),
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.contentInset = UIEdgeInsets(top: -view.safeAreaInsets.top, left: 0, bottom: 0, right: 0)
tableView.tableHeaderView?.autoresizingMask = []
tableView.tableHeaderView?.layoutIfNeeded()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
}
}
The reason your "blue view" is overlapping the cells is because you are constraining its Top to the red view's Bottom, but you're not updating the header view size.
One good approach is to create a UIView subclass to use as your header view. Setup all of its content with proper auto-layout constraints.
Then, in the controller's viewDidLayoutSubviews(), we use .systemLayoutSizeFitting(...) to determine the header view's height and update its frame:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// update table header size
guard let headerView = tableView.tableHeaderView else { return }
let height = headerView.systemLayoutSizeFitting(CGSize(width: tableView.frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow).height
var frame = headerView.frame
// avoids infinite loop!
if height != frame.height {
frame.size.height = height
headerView.frame = frame
tableView.tableHeaderView = headerView
}
}
Here is a complete example...
First, our custom view class:
class SampleHeaderView: UIView {
let redView: UIView = {
let v = UIView()
v.backgroundColor = .systemRed
return v
}()
let blueView: UIView = {
let v = UIView()
v.backgroundColor = .systemBlue
return v
}()
let redTopLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .yellow
v.numberOfLines = 0
return v
}()
let redBottomLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .green
v.numberOfLines = 0
return v
}()
let multiLineLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .cyan
v.numberOfLines = 0
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// all views will use auto-layout
[redView, blueView, redTopLabel, redBottomLabel, multiLineLabel].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
// prevent label vertical compression
[redTopLabel, redBottomLabel, multiLineLabel].forEach { v in
v.setContentCompressionResistancePriority(.required, for: .vertical)
}
// add top and bottom labels to red view
redView.addSubview(redTopLabel)
redView.addSubview(redBottomLabel)
// add multi-line label to blue view
blueView.addSubview(multiLineLabel)
// add red and blue views to self
addSubview(redView)
addSubview(blueView)
// the following constraints need to have less-than required to avoid
// auto-layout warnings
// blue view bottom to self
let c1 = blueView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0)
// labels trailing contraints
let c2 = redTopLabel.trailingAnchor.constraint(equalTo: redView.trailingAnchor, constant: -8.0)
let c3 = redBottomLabel.trailingAnchor.constraint(equalTo: redView.trailingAnchor, constant: -8.0)
let c4 = multiLineLabel.trailingAnchor.constraint(equalTo: blueView.trailingAnchor, constant: -8.0)
[c1, c2, c3, c4].forEach { c in
c.priority = .required - 1
}
NSLayoutConstraint.activate([
// red view top to self
redView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
// leading / trailing to self
redView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
redView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
// blue view top to red view bottom
blueView.topAnchor.constraint(equalTo: redView.bottomAnchor, constant: 0.0),
// leading / trailing to self
blueView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
blueView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
// top and bottom labels, constrained in red view
// with a little "padding"
redTopLabel.topAnchor.constraint(equalTo: redView.topAnchor, constant: 8.0),
redTopLabel.leadingAnchor.constraint(equalTo: redView.leadingAnchor, constant: 8.0),
redBottomLabel.topAnchor.constraint(equalTo: redTopLabel.bottomAnchor, constant: 8.0),
redBottomLabel.leadingAnchor.constraint(equalTo: redView.leadingAnchor, constant: 8.0),
redBottomLabel.bottomAnchor.constraint(equalTo: redView.bottomAnchor, constant: -8.0),
// multi-line label, constrained in blue view
// with a little "padding"
multiLineLabel.topAnchor.constraint(equalTo: blueView.topAnchor, constant: 8.0),
multiLineLabel.leadingAnchor.constraint(equalTo: blueView.leadingAnchor, constant: 8.0),
multiLineLabel.bottomAnchor.constraint(equalTo: blueView.bottomAnchor, constant: -8.0),
// the less-than-required priority constraints
c1, c2, c3, c4,
])
}
}
and a sample controller:
class TableHeaderViewController: UIViewController {
var sampleHeaderView = SampleHeaderView()
private(set) lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
sampleHeaderView.redTopLabel.text = "The Red Top Label"
sampleHeaderView.redBottomLabel.text = "The Red Bottom Label, with enough text that is should wrap."
sampleHeaderView.multiLineLabel.text = "This text is for the Label in the Blue View. It is also long enough that it will require word-wrapping. Note that the header updates itself when the frame changes, such as on device rotation."
tableView.tableHeaderView = sampleHeaderView
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// update table header size
guard let headerView = tableView.tableHeaderView else { return }
let height = headerView.systemLayoutSizeFitting(CGSize(width: tableView.frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow).height
var frame = headerView.frame
// avoids infinite loop!
if height != frame.height {
frame.size.height = height
headerView.frame = frame
tableView.tableHeaderView = headerView
}
}
}
extension TableHeaderViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 20
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
c.textLabel?.text = "\(indexPath)"
return c
}
}
Output:
and rotated:

Cannot move UILabel position programatically

I have a file for the View section of app, where i have all the labels and images that i intent to use, this is what i have in my DetailViewTableCell class, which inherits from UIView.
class DetailViewTableCell: UIView {
var detailMainImage: UIImageView = UIImageView()
var detailName: UILabel = UILabel()
var detailType: UILabel = UILabel()
var detailHeart: UIImageView = UIImageView()
}
Now i move to my DetailViewController class, here i try and add the label, the label is added but it appears always at top left corner at 0,0 coordinate, when i try and add constraints for position, i always get error, now i can try
detailMain.detailName.frame.origin.x = 30
but i get error:
Constraint items must each be a view or layout guide.
In any case i do not wish to use this approach but more something like this
NSLayoutConstraint(item: detailMain.detailName, attribute: .leading, relatedBy: .equal, toItem: detailMain.detailName.superview, attribute: .leading, multiplier: 1, constant: 20).isActive = true
but i get the same above error, my over all code is this:
self.view.addSubview(detailMain.detailName)
detailMain.detailName.translatesAutoresizingMaskIntoConstraints = false
detailMain.detailName.heightAnchor.constraint(equalToConstant: 25).isActive = true
detailMain.detailName.widthAnchor.constraint(greaterThanOrEqualToConstant: 100).isActive = true
detailMain.detailName.font = UIFont(name: "Rubik-Medium", size: 30)
detailMain.detailName.backgroundColor = UIColor.white
detailMain.detailName.textColor = UIColor.black
Which works perfectly fine but the moment i try and constraints, the error come up, this is how the app shows up with out constraints and name at top most left corner
////////UPDATE
So here is my new DetailViewTableCell,
import UIKit
class DetailViewTableCell: UIView {
var detailMainImage: UIImageView = UIImageView()
var detailName: UILabel = UILabel()
var detailType: UILabel = UILabel()
var detailHeart: UIImageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
[detailMainImage, detailName, detailType, detailHeart].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
NSLayoutConstraint.activate([
// constrain main image to all 4 sides
detailMainImage.topAnchor.constraint(equalTo: topAnchor),
detailMainImage.leadingAnchor.constraint(equalTo: leadingAnchor),
detailMainImage.trailingAnchor.constraint(equalTo: trailingAnchor),
detailMainImage.bottomAnchor.constraint(equalTo: bottomAnchor),
// activate the height contraint
// constrain detailType label
// 30-pts from Leading
// 12-pts from Top
detailType.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30.0),
detailType.topAnchor.constraint(equalTo: topAnchor, constant: 12.0),
// constrain detailName label
// 30-pts from Leading
// 12-pts from Bottom
detailName.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30.0),
detailName.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12.0),
// constrain detailHeart image
// 12-pts from Trailing
// 12-pts from Bottom
// width: 24 height: equal to width (1:1 square)
detailHeart.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
detailHeart.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12),
detailHeart.widthAnchor.constraint(equalToConstant: 24),
detailHeart.heightAnchor.constraint(equalTo: detailHeart.widthAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
and this 2 lines is what i add to my Detail view controller in viewDidLoad
let v = DetailViewTableCell()
detailTableView.tableHeaderView = v
Also i add this function
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// this is needed to allow the header view's content
// to determine its height
guard let headerView = detailTableView.tableHeaderView else {
return
}
let size = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
if headerView.frame.size.height != size.height {
headerView.frame.size.height = size.height
detailTableView.tableHeaderView = headerView
detailTableView.layoutIfNeeded()
}
}
then in my viewForHeaderInSection inbuilt function i add this
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView()
tableView.rowHeight = 80
headerView.translatesAutoresizingMaskIntoConstraints = false
headerView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
headerView.heightAnchor.constraint(equalToConstant: 400).isActive = true
detailMain.detailMainImage.translatesAutoresizingMaskIntoConstraints = false
detailMain.detailMainImage.heightAnchor.constraint(equalToConstant: 400).isActive = true
detailMain.detailMainImage.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
detailMain.detailMainImage.image = UIImage(named: restaurant.image)
detailMain.detailMainImage.contentMode = .scaleAspectFill
detailMain.detailMainImage.clipsToBounds = true
headerView.addSubview(detailMain.detailMainImage)
//Add the name
detailMain.detailName.text = restaurant.name
headerView.addSubview(detailMain.detailName)
return headerView
}
but still same position , is there any thing i add to add or remove from my code
You didn't show where you *want the labels, but this should get you going...
In your "header view" class:
add your elements: detailMainImage, detailName, etc...
set their properties and constraints as desired
You can get auto-layout to use the constraints you've setup in the header view to automatically determine its height:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// this is needed to allow the header view's content
// to determine its height
guard let headerView = tableView.tableHeaderView else {
return
}
let size = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
if headerView.frame.size.height != size.height {
headerView.frame.size.height = size.height
tableView.tableHeaderView = headerView
tableView.layoutIfNeeded()
}
}
So, here's a complete example:
class TestHeaderTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
// instantiate the header view
let v = DetailTableHeaderView()
// set it as the tableHeaderView
tableView.tableHeaderView = v
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// this is needed to allow the header view's content
// to determine its height
guard let headerView = tableView.tableHeaderView else {
return
}
let size = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
if headerView.frame.size.height != size.height {
headerView.frame.size.height = size.height
tableView.tableHeaderView = headerView
tableView.layoutIfNeeded()
}
}
}
class DetailTableHeaderView: UIView {
var detailMainImage: UIImageView = UIImageView()
var detailName: UILabel = UILabel()
var detailType: UILabel = UILabel()
var detailHeart: UIImageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .white
// give the heart image view a background color so we can see its frame
detailHeart.backgroundColor = .red
if let img = UIImage(named: "teacup") {
detailMainImage.image = img
}
// give labels some text so we can see them
detailType.text = "Detail Type"
detailName.text = "Detail Name"
// setup fonts for labels as desired
//detailName.font = UIFont(name: "Rubik-Medium", size: 30)
// I don't have "Rubik" so this is with the system font
detailName.font = UIFont.systemFont(ofSize: 30, weight: .bold)
detailName.backgroundColor = UIColor.white
detailName.textColor = UIColor.black
detailType.textColor = .white
[detailMainImage, detailName, detailType, detailHeart].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
// give main image a height
// set its Priority to 999 to prevent layout constraint conflict warnings
let mainImageHeightAnchor = detailMainImage.heightAnchor.constraint(equalToConstant: 300.0)
mainImageHeightAnchor.priority = UILayoutPriority(rawValue: 999)
NSLayoutConstraint.activate([
// constrain main image to all 4 sides
detailMainImage.topAnchor.constraint(equalTo: topAnchor),
detailMainImage.leadingAnchor.constraint(equalTo: leadingAnchor),
detailMainImage.trailingAnchor.constraint(equalTo: trailingAnchor),
detailMainImage.bottomAnchor.constraint(equalTo: bottomAnchor),
// activate the height contraint
mainImageHeightAnchor,
// constrain detailType label
// 30-pts from Leading
// 12-pts from Top
detailType.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30.0),
detailType.topAnchor.constraint(equalTo: topAnchor, constant: 12.0),
// constrain detailName label
// 30-pts from Leading
// 12-pts from Bottom
detailName.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30.0),
detailName.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12.0),
// constrain detailHeart image
// 12-pts from Trailing
// 12-pts from Bottom
// width: 24 height: equal to width (1:1 square)
detailHeart.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
detailHeart.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12),
detailHeart.widthAnchor.constraint(equalToConstant: 24),
detailHeart.heightAnchor.constraint(equalTo: detailHeart.widthAnchor),
])
}
}
For this example, I just set the background color of the "heart" image view to red, and I clipped the teacup out of your image:
And this is the result:
Edit - to use the custom view as a Section header view...
Use the same DetailTableHeaderView class from above, but change the table view controller as follows:
class TestSectionHeaderTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.sectionHeaderHeight = UITableView.automaticDimension
tableView.estimatedSectionHeaderHeight = 300
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 {
let v = DetailTableHeaderView()
// for example implementation...
if let img = UIImage(named: "teacup") {
v.detailMainImage.image = img
}
v.detailName.text = "Testing the Name"
// for your implementation...
//if let img = UIImage(named: restaurant.image) {
// v.detailMainImage.image = img
//}
//v.detailName.text = restaurant.name
return v
}
return nil;
}
}

How to Implement UITextView inside UITableView inside UITableView Swift

how to implement UITextView inside UITableView inside UITableView ??
(1) If you type text in 'UITextView', the height of 'UITableViewCell2' is automatically increased.
(2) When the height of 'UITableViewCell2' is increased, the height of 'UITableViewCell' is automatically increased accordingly.
I have implemented the case of (1) but not (2).
How should I implement it?
Nesting table views may not be the ideal solution for this, but your UITableViewCell would need to estimate and measure the whole height of the embedded UITableView, and propagate changes up to the parent table.
You might want to give this a try...
It using a single-section table view. Each cell contains a UIStackView that arranges the (variable) UITextViews.
No #IBOutlet or #IBAction or prototype cell connections... just assign a standard UIViewController custom class to TableTextViewsViewController:
//
// TableTextViewsViewController.swift
// Created by Don Mag on 3/10/20.
//
import UIKit
class TextViewsCell: UITableViewCell, UITextViewDelegate {
let frameView: UIView = {
let v = UIView()
v.backgroundColor = .clear
v.layer.borderColor = UIColor(red: 0.0, green: 0.5, blue: 0.0, alpha: 1.0).cgColor
v.layer.borderWidth = 1
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let stackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 8
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let stackViewPadding: CGFloat = 8.0
var textViewCosure: ((Int, String)->())?
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 {
let g = contentView.layoutMarginsGuide
contentView.addSubview(frameView)
frameView.addSubview(stackView)
// bottom constraint needs to be less than 1000 (required) to avoid auot-layout warnings
let frameViewBottomConstrait = frameView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0)
frameViewBottomConstrait.priority = UILayoutPriority(rawValue: 999)
NSLayoutConstraint.activate([
frameView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
frameView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
frameView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
frameViewBottomConstrait,
stackView.topAnchor.constraint(equalTo: frameView.topAnchor, constant: stackViewPadding),
stackView.leadingAnchor.constraint(equalTo: frameView.leadingAnchor, constant: stackViewPadding),
stackView.trailingAnchor.constraint(equalTo: frameView.trailingAnchor, constant: -stackViewPadding),
stackView.bottomAnchor.constraint(equalTo: frameView.bottomAnchor, constant: -stackViewPadding),
])
}
override func prepareForReuse() {
super.prepareForReuse()
stackView.arrangedSubviews.forEach {
$0.removeFromSuperview()
}
}
func fillData(_ strings: [String]) -> Void {
strings.forEach {
let v = UITextView()
v.font = UIFont.systemFont(ofSize: 16.0)
v.isScrollEnabled = false
// hugging and compression resistance set to required for cell expansion animation
v.setContentHuggingPriority(.required, for: .vertical)
v.setContentCompressionResistancePriority(.required, for: .vertical)
v.text = $0
// frame the text view
v.layer.borderColor = UIColor.blue.cgColor
v.layer.borderWidth = 1
v.delegate = self
stackView.addArrangedSubview(v)
}
}
func textViewDidChange(_ textView: UITextView) {
guard let idx = stackView.arrangedSubviews.firstIndex(of: textView) else {
fatalError("Shouldn't happen, but couldn't find the textView index")
}
textViewCosure?(idx, textView.text)
}
}
class TableTextViewsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let topLabel: UILabel = {
let v = UILabel()
v.text = "Top Label"
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let tableView: UITableView = {
let v = UITableView()
v.layer.borderColor = UIColor.red.cgColor
v.layer.borderWidth = 1
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
var myData: [[String]] = [[String]]()
var textViewsInRows: [Int] = [
3, 4, 2, 6, 1, 4, 3,
]
override func viewDidLoad() {
super.viewDidLoad()
// generate some dummy data
var i = 1
textViewsInRows.forEach {
var s: [String] = [String]()
for j in 1...$0 {
s.append("Table Row: \(i) TextView \(j)")
}
myData.append(s)
i += 1
}
view.addSubview(topLabel)
view.addSubview(tableView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
topLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
topLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
topLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
tableView.topAnchor.constraint(equalTo: topLabel.bottomAnchor, constant: 8.0),
tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -8.0),
])
tableView.dataSource = self
tableView.delegate = self
tableView.separatorStyle = .none
tableView.keyboardDismissMode = .onDrag
tableView.register(TextViewsCell.self, forCellReuseIdentifier: "TextViewsCell")
}
// MARK: - Table view data source
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TextViewsCell", for: indexPath) as! TextViewsCell
cell.fillData(myData[indexPath.row])
cell.textViewCosure = { [weak self] idx, str in
// update our data
self?.myData[indexPath.row][idx] = str
// update table view cell height
self?.tableView.beginUpdates()
self?.tableView.endUpdates()
}
return cell
}
}
Result - red border is the tableView, green border is each cell's contentView, blue border is each textView:

Resources