How to set width of a UITableView equal to the maximum cells' width inside of it? - uitableview

I have a table view and I using a custom cell which has 3 UI elements as subviews. I have made my UIelements which are labels to shrink as per there content size. Now my problem is to set cell to shrink as per its UIElements and relatively adjust tableview width.

One way could be to:
create a separate view e.g. named ThreeElementView which will be added to the content view of the cell
with all rows available you could call systemLayoutSizeFitting to get the maximum width
add an width constraint (NSLayoutConstraint) to the table view
if the data of the table view changes, adjust the constraint
The widthContraint can be setup like this:
private var widthContraint: NSLayoutConstraint?
widthContraint = tableView.widthAnchor.constraint(equalToConstant: 128)
widthContraint?.isActive = true
if let width = calcWidth() {
widthContraint?.constant = width
}
You would also call the last 3 lines before updating the table view with tableView.reloadData().
Assuming that data contains the actual table data, the width calculation could look like this:
private func calcWidth() -> CGFloat? {
let prototypeView = ThreeElementView()
let widths = data.map { row -> CGFloat in
prototypeView.label1.text = row[0]
prototypeView.label2.text = row[1]
prototypeView.label3.text = row[2]
prototypeView.setNeedsLayout()
prototypeView.layoutIfNeeded()
return prototypeView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).width
}
return widths.max()
}
So for each line you would calculate the width of the contents and finally return the maximum value.
Self-Contained Test
Here is a self-contained test of the above. The UI has been built programmatically in code so that the result is easier to follow. If you press the button, you can see that the width of the tableview then also dynamically adjusts just by setting the constraint.
ThreeElementView.swift
import UIKit
class ThreeElementView: UIView {
let label1 = UILabel()
let label2 = UILabel()
let label3 = UILabel()
init() {
super.init(frame: .zero)
label1.backgroundColor = UIColor(red: 84/255, green: 73/255, blue: 75/255, alpha: 1.0)
label1.textColor = .white
label2.backgroundColor = UIColor(red: 131/255, green: 151/255, blue: 136/255, alpha: 1.0)
label2.textColor = .white
label3.backgroundColor = UIColor(red: 189/255, green: 187/255, blue: 182/255, alpha: 1.0)
label1.translatesAutoresizingMaskIntoConstraints = false
label2.translatesAutoresizingMaskIntoConstraints = false
label3.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(label1)
self.addSubview(label2)
self.addSubview(label3)
NSLayoutConstraint.activate([
label1.leadingAnchor.constraint(equalTo: self.leadingAnchor),
label1.topAnchor.constraint(equalTo: self.topAnchor),
label1.bottomAnchor.constraint(equalTo: self.bottomAnchor),
label2.leadingAnchor.constraint(equalTo: label1.trailingAnchor),
label2.topAnchor.constraint(equalTo: self.topAnchor),
label2.bottomAnchor.constraint(equalTo: self.bottomAnchor),
label3.leadingAnchor.constraint(equalTo: label2.trailingAnchor),
label3.topAnchor.constraint(equalTo: self.topAnchor),
label3.bottomAnchor.constraint(equalTo: self.bottomAnchor),
label3.trailingAnchor.constraint(equalTo: self.trailingAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
ThreeElementCell.swift
import UIKit
class ThreeElementCell: UITableViewCell {
static let id = "ThreeElementCellId"
let threeElementView = ThreeElementView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
threeElementView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(threeElementView)
NSLayoutConstraint.activate([
threeElementView.topAnchor.constraint(equalTo: contentView.topAnchor),
threeElementView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
threeElementView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
threeElementView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
ViewController.swift
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
private let tableView = UITableView()
private let addMoreButton = UIButton()
private var data = [
["a", "tiny", "row"],
]
private var widthContraint: NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
setupButton()
}
#objc func onAddMore() {
if data.count < 2 {
data.append(["a", "little bit", "longer row"])
} else {
data.append(["this is", " finally an even longer", "row"])
}
if let width = calcWidth() {
widthContraint?.constant = width
}
tableView.reloadData()
}
// MARK: - UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: ThreeElementCell.id, for: indexPath) as! ThreeElementCell
let item = data[indexPath.row]
cell.threeElementView.label1.text = item[0]
cell.threeElementView.label2.text = item[1]
cell.threeElementView.label3.text = item[2]
return cell
}
// MARK: - Private
private func setupTableView() {
tableView.backgroundColor = UIColor(red: 245/255, green: 228/255, blue: 215/255, alpha: 1.0)
tableView.register(ThreeElementCell.self, forCellReuseIdentifier: ThreeElementCell.id)
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16.0),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16.0),
])
widthContraint = tableView.widthAnchor.constraint(equalToConstant: 128)
widthContraint?.isActive = true
if let width = calcWidth() {
widthContraint?.constant = width
}
tableView.delegate = self
tableView.dataSource = self
}
private func setupButton() {
addMoreButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(addMoreButton)
NSLayoutConstraint.activate([
addMoreButton.topAnchor.constraint(equalTo: tableView.bottomAnchor, constant: 32.0),
addMoreButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
addMoreButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -32.0),
])
addMoreButton.setTitle("Add More Rows", for: .normal)
addMoreButton.setTitleColor(.blue, for: .normal)
addMoreButton.addTarget(self, action: #selector(onAddMore), for: .touchUpInside)
}
private func calcWidth() -> CGFloat? {
let prototypeView = ThreeElementView()
let widths = data.map { row -> CGFloat in
prototypeView.label1.text = row[0]
prototypeView.label2.text = row[1]
prototypeView.label3.text = row[2]
prototypeView.setNeedsLayout()
prototypeView.layoutIfNeeded()
return prototypeView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).width
}
return widths.max()
}
}
Demo

Related

TableView Cells not formatting correctly unless image is cached

I am having an issue with my tableview, where the cells don't orient correctly before an image is cached, and only once I return back to a page and my image is cached do they orient correctly. Here is an example of what I am talking about, the first image is when I first go to the page, and I have a function which stores images in an imagecache, and the second picture is when I return to that page after navigating somewhere else, and the images are cached.
Incorrectly Formatted:
Correctly Formatted:
I know it is a very subtle difference
with the way it looks
but I'd love to get that figured out and understand why this is happening
My cells constant height is 85, and here is the relevant code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//Indexing part I left out dw about this
let cell = discountTableView.dequeueReusableCell(withIdentifier: self.discountCellId, for: indexPath) as! DiscountCell
cell.textLabel?.text = discount.retailerName
cell.detailTextLabel?.text = discount.matchedFirstName + " " + discount.matchedLastName
cell.profileImageView.image = UIImage.gif(name: "imageLoading")!
grabImageWithCache(discount.retailerImageLink) { (profilePic) in
cell.profileImageView.image = profilePic
}
//Adding second Image
cell.matchImageView.image = UIImage.gif(name: "imageLoading")!
grabImageWithCache(discount.matchedProfileImage) { (matchProfilepic) in
cell.matchImageView.image = matchProfilepic
}
//declare no selection style for cell (avoid gray highlight)
cell.selectionStyle = .none
//format the cell's curvature
cell.layer.cornerRadius = 38
return cell
}
**I am curious as to why the height of the cells seem to change when the image is cached, because the height is set to a constant number, so I have no idea why it is changing. When I print the height of the cells it says it is 85 both times as well...
UITableViewCell Class:
class DiscountCell: UITableViewCell {
override func layoutSubviews() {
super.layoutSubviews()
//vertical spacing between cells
let padding = UIEdgeInsets(top: 0, left: 0, bottom: 7, right: 0)
bounds = bounds.inset(by: padding)
textLabel?.frame = CGRect(x: 120, y: textLabel!.frame.origin.y-10, width: textLabel!.frame.width, height: textLabel!.frame.height)
detailTextLabel?.frame = CGRect(x: 120, y: detailTextLabel!.frame.origin.y, width: detailTextLabel!.frame.width, height: detailTextLabel!.frame.height)
//spacing between cells
let margins = UIEdgeInsets(top: 0, left: 0, bottom: 85, right: 0)
contentView.frame = contentView.frame.inset(by: margins)
}
//sets a placeholder imageview
let profileImageView: UIImageView = {
let imageView = UIImageView ()
imageView.image = UIImage(named: "failed")
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.layer.cornerRadius = 35
imageView.layer.masksToBounds = true
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.borderWidth = 2
imageView.layer.borderColor = #colorLiteral(red: 0.9725490196, green: 0.9725490196, blue: 0.9725490196, alpha: 1)
return imageView
}()
//PlaceHolder imageview for match
let matchImageView:UIImageView = {
let imageView = UIImageView ()
imageView.image = UIImage(named: "failed")
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.layer.cornerRadius = 35
imageView.layer.masksToBounds = true
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
return imageView
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
addSubview(matchImageView)
addSubview(profileImageView)
//Setting Profile Pic anchors
profileImageView.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 5).isActive = true
profileImageView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
profileImageView.widthAnchor.constraint(equalToConstant: 70).isActive = true
profileImageView.heightAnchor.constraint(equalToConstant: 70).isActive = true
//Setting Match Anchors
matchImageView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
matchImageView.widthAnchor.constraint(equalToConstant: 70).isActive = true
matchImageView.heightAnchor.constraint(equalToConstant: 70).isActive = true
matchImageView.leftAnchor.constraint(equalTo: profileImageView.leftAnchor,constant: 35).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I believe I have narrowed in on the issue, I noticed that the spacing between the cells was inconsistent both times, so I think the issue is with these lines of code
//vertical spacing between cells
let padding = UIEdgeInsets(top: 0, left: 0, bottom: 7, right: 0)
bounds = bounds.inset(by: padding)
I am not sure what to do differently, as I watched tutorials saying to change the contentviews insets but when I do, it has no effect, and I see things saying iOS 11 changed the way you do this, but haven't been able to find how to actually go about fixing this.
Couple notes...
Not a good idea to modify a cell's bounds or its contentView. UIKit uses those for a lot of things (such as adjusting contents when implementing table editing).
Cell subviews should be added to the cell's contentView, not to the cell itself. Again, due to how table views rely on that hierarchy.
You can use UIView and UIImageView subclasses to handle the "rounding" for you. For example:
class RoundImageView: UIImageView {
override func layoutSubviews() {
layer.cornerRadius = bounds.size.height * 0.5
}
}
class RoundEndView: UIView {
override func layoutSubviews() {
layer.cornerRadius = bounds.size.height * 0.5
}
}
Here is a complete implementation:
class RoundImageView: UIImageView {
override func layoutSubviews() {
layer.cornerRadius = bounds.size.height * 0.5
}
}
class RoundEndView: UIView {
override func layoutSubviews() {
layer.cornerRadius = bounds.size.height * 0.5
}
}
class DiscountCell: UITableViewCell {
// "Holder" view... contains all other UI elements
// use RoundEndView so it handles the corner radius by itself
let holderView: RoundEndView = {
let v = RoundEndView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .white
return v
}()
//sets a placeholder imageview
// use RoundImageView so it handles the corner radius by itself
let profileImageView: RoundImageView = {
let imageView = RoundImageView()
imageView.image = UIImage(named: "failed")
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.layer.masksToBounds = true
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.borderWidth = 2
imageView.layer.borderColor = #colorLiteral(red: 0.9725490196, green: 0.9725490196, blue: 0.9725490196, alpha: 1)
return imageView
}()
//PlaceHolder imageview for match
// use RoundImageView so it handles the corner radius by itself
let matchImageView: RoundImageView = {
let imageView = RoundImageView()
imageView.image = UIImage(named: "failed")
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.layer.masksToBounds = true
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
return imageView
}()
let mainLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.font = UIFont.systemFont(ofSize: 17.0, weight: .regular)
return v
}()
let subLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.font = UIFont.systemFont(ofSize: 14.0, weight: .regular)
return v
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .clear
contentView.addSubview(holderView)
holderView.addSubview(matchImageView)
holderView.addSubview(profileImageView)
holderView.addSubview(mainLabel)
holderView.addSubview(subLabel)
NSLayoutConstraint.activate([
// holder view constraints Top 5 pts from contentView Top
holderView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5.0),
// Leading 12 pts from contentView Leading
holderView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12.0),
// Trailing 5 pts from contentView Trailing
holderView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -5.0),
// 5 pts from contentView Bottom (use lessThanOrEqualTo to avoid auto-layout warnings)
holderView.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -5.0),
//Setting Profile Pic anchors - Top and Leading 5 pts from Top and Leading of Holder view
profileImageView.topAnchor.constraint(equalTo: holderView.topAnchor, constant: 5),
profileImageView.leadingAnchor.constraint(equalTo: holderView.leadingAnchor, constant: 5),
// Botom 5 pts from contentView Bottom - this will determine the height of the Holder view
profileImageView.bottomAnchor.constraint(equalTo: holderView.bottomAnchor, constant: -5),
// width 70 pts, height equal to width (keeps it "round")
profileImageView.widthAnchor.constraint(equalToConstant: 70.0),
profileImageView.heightAnchor.constraint(equalTo: profileImageView.widthAnchor),
//Setting Match Pic Anchors - 35 pts from Profile Leading, centerY to Profile
matchImageView.leadingAnchor.constraint(equalTo: profileImageView.leadingAnchor, constant: 35),
matchImageView.centerYAnchor.constraint(equalTo: profileImageView.centerYAnchor, constant: 0.0),
// same width and height as Profile
matchImageView.widthAnchor.constraint(equalTo: profileImageView.widthAnchor),
matchImageView.heightAnchor.constraint(equalTo: profileImageView.heightAnchor),
//Setting Main Label Anchors - Top equal to Top of Match image
mainLabel.topAnchor.constraint(equalTo: matchImageView.topAnchor, constant: 0.0),
// Leading 12 pts from Match image Trailing
mainLabel.leadingAnchor.constraint(equalTo: matchImageView.trailingAnchor, constant: 12.0),
// prevent Trailing from going past holder view Trailing
mainLabel.trailingAnchor.constraint(equalTo: holderView.trailingAnchor, constant: -16.0),
//Setting Sub Label Anchors - Top 8 pts from Main label Bottom
subLabel.topAnchor.constraint(equalTo: mainLabel.bottomAnchor, constant: 8.0),
// Leading and Trailing equal to Main label Leading / Trailing
subLabel.leadingAnchor.constraint(equalTo: mainLabel.leadingAnchor, constant: 0.0),
subLabel.trailingAnchor.constraint(equalTo: mainLabel.trailingAnchor, constant: 0.0),
])
}
}
// example Discount object struct
struct Discount {
var retailerName: String = ""
var matchedFirstName: String = ""
var matchedLastName: String = ""
var matchedProfileImage: String = ""
var retailerImageLink: String = ""
}
class DiscountViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let discountTableView = UITableView()
let discountCellId = "dcID"
var myData: [Discount] = []
override func viewDidLoad() {
super.viewDidLoad()
// a little sample data
var d = Discount(retailerName: "Cup & Cone", matchedFirstName: "Kara", matchedLastName: "Tomlinson", matchedProfileImage: "pro1", retailerImageLink: "")
myData.append(d)
d = Discount(retailerName: "Cup & Cone", matchedFirstName: "Sophie", matchedLastName: "Turner", matchedProfileImage: "pro2", retailerImageLink: "")
myData.append(d)
d = Discount(retailerName: "Another Retailer", matchedFirstName: "WanaB3", matchedLastName: "Nerd", matchedProfileImage: "pro3", retailerImageLink: "")
myData.append(d)
discountTableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(discountTableView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
discountTableView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
discountTableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
discountTableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
discountTableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
view.backgroundColor = UIColor(red: 1.0, green: 0.9, blue: 0.7, alpha: 1.0)
discountTableView.backgroundColor = UIColor(red: 0.66, green: 0.66, blue: 0.9, alpha: 1.0)
discountTableView.separatorStyle = .none
discountTableView.delegate = self
discountTableView.dataSource = self
discountTableView.register(DiscountCell.self, forCellReuseIdentifier: discountCellId)
}
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 {
//Indexing part I left out dw about this
let discount = myData[indexPath.row]
let cell = discountTableView.dequeueReusableCell(withIdentifier: self.discountCellId, for: indexPath) as! DiscountCell
cell.mainLabel.text = discount.retailerName
cell.subLabel.text = discount.matchedFirstName + " " + discount.matchedLastName
// add first image
if let img = UIImage(named: "imageLoading") {
cell.profileImageView.image = img
}
// use your custom image funcs
//cell.profileImageView.image = UIImage.gif(name: "imageLoading")!
//grabImageWithCache(discount.retailerImageLink) { (profilePic) in
// cell.profileImageView.image = profilePic
//}
//Adding second Image
if let img = UIImage(named: discount.matchedProfileImage) {
cell.matchImageView.image = img
}
// use your custom image funcs
//cell.matchImageView.image = UIImage.gif(name: "imageLoading")!
//grabImageWithCache(discount.matchedProfileImage) { (matchProfilepic) in
// cell.matchImageView.image = matchProfilepic
//}
//declare no selection style for cell (avoid gray highlight)
cell.selectionStyle = .none
return cell
}
}
Results:
Your cells are a pill shape, which means you can achieve the same effect by doing cell.layer.cornerRadius = cell.frame.height / 2. Next you should use top and bottom constraints for your profileImageView so it forces the cell to have a certain padding instead of trying to create the padding yourself. The problem is that your cell height is changing a little bit when the images are cached, no idea what could be causing that.

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:

Multilinelabel inside multiple stackviews inside UITableViewCell

I have view hierarchy like below;
UITableViewCell ->
-> UIView -> UIStackView (axis: vertical, distribution: fill)
-> UIStackView (axis: horizontal, alignment: top, distribution: fillEqually)
-> UIView -> UIStackView(axis:vertical, distribution: fill)
-> TwoLabelView
My problem is that labels don't get more than one line. I read every question in SO and also tried every possibility but none of them worked. On below screenshot, on the top left box, there should be two pair of label but even one of them isn't showing.
My Question is that how can I achieve multiline in the first box (both for left and right)?
If I change top stack views distribution to fillProportionally, labels get multiline but there will be a gap between last element of first box and the box itself
My first top stack views
//This is the Stackview used just below UITableViewCell
private let stackView: UIStackView = {
let s = UIStackView()
s.distribution = .fill
s.axis = .vertical
s.spacing = 10
s.translatesAutoresizingMaskIntoConstraints = false
return s
}()
//This is used to create two horizontal box next to each other
private let myStackView: UIStackView = {
let s = UIStackView()
s.distribution = .fillEqually
s.spacing = 10
s.axis = .horizontal
//s.alignment = .center
s.translatesAutoresizingMaskIntoConstraints = false
return s
}()
UILabel Class:
fileprivate class FixAutoLabel: UILabel {
override func layoutSubviews() {
super.layoutSubviews()
if(self.preferredMaxLayoutWidth != self.bounds.size.width) {
self.preferredMaxLayoutWidth = self.bounds.size.width
}
}
}
#IBDesignable class TwoLabelView: UIView {
var topMargin: CGFloat = 0.0
var verticalSpacing: CGFloat = 3.0
var bottomMargin: CGFloat = 0.0
#IBInspectable var firstLabelText: String = "" { didSet { updateView() } }
#IBInspectable var secondLabelText: String = "" { didSet { updateView() } }
fileprivate var firstLabel: FixAutoLabel!
fileprivate var secondLabel: FixAutoLabel!
override init(frame: CGRect) {
super.init(frame: frame)
setUpView()
}
required public init?(coder: NSCoder) {
super.init(coder:coder)
setUpView()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setUpView()
}
func setUpView() {
firstLabel = FixAutoLabel()
firstLabel.font = UIFont.systemFont(ofSize: 18.0, weight: UIFont.Weight.bold)
firstLabel.numberOfLines = 0
firstLabel.lineBreakMode = NSLineBreakMode.byTruncatingTail
secondLabel = FixAutoLabel()
secondLabel.font = UIFont.systemFont(ofSize: 13.0, weight: UIFont.Weight.regular)
secondLabel.numberOfLines = 1
secondLabel.lineBreakMode = NSLineBreakMode.byTruncatingTail
addSubview(firstLabel)
addSubview(secondLabel)
// we're going to set the constraints
firstLabel .translatesAutoresizingMaskIntoConstraints = false
secondLabel.translatesAutoresizingMaskIntoConstraints = false
// pin both labels' left-edges to left-edge of self
firstLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0).isActive = true
secondLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0).isActive = true
// pin both labels' right-edges to right-edge of self
firstLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0).isActive = true
secondLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0).isActive = true
// pin firstLabel to the top of self + topMargin (padding)
firstLabel.topAnchor.constraint(equalTo: topAnchor, constant: topMargin).isActive = true
// pin top of secondLabel to bottom of firstLabel + verticalSpacing
secondLabel.topAnchor.constraint(equalTo: firstLabel.bottomAnchor, constant: verticalSpacing).isActive = true
// pin bottom of self to bottom of secondLabel + bottomMargin (padding)
bottomAnchor.constraint(equalTo: secondLabel.bottomAnchor, constant: bottomMargin).isActive = true
// call common "refresh" func
updateView()
}
func updateView() {
firstLabel.preferredMaxLayoutWidth = self.bounds.width
secondLabel.preferredMaxLayoutWidth = self.bounds.width
firstLabel.text = firstLabelText
secondLabel.text = secondLabelText
firstLabel.sizeToFit()
secondLabel.sizeToFit()
setNeedsUpdateConstraints()
}
override open var intrinsicContentSize : CGSize {
// just has to have SOME intrinsic content size defined
// this will be overridden by the constraints
return CGSize(width: 1, height: 1)
}
}
UIView -> UIStackView class
class ViewWithStack: UIView {
let verticalStackView: UIStackView = {
let s = UIStackView()
s.distribution = .fillEqually
s.spacing = 10
s.axis = .vertical
s.translatesAutoresizingMaskIntoConstraints = false
return s
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = UIColor.white
self.layer.cornerRadius = 6.0
self.layer.applySketchShadow(color: UIColor(red:0.56, green:0.56, blue:0.56, alpha:1), alpha: 0.2, x: 0, y: 0, blur: 10, spread: 0)
addSubview(verticalStackView)
let lessThan = verticalStackView.bottomAnchor.constraint(lessThanOrEqualTo: self.bottomAnchor, constant: 0)
lessThan.priority = UILayoutPriority(1000)
lessThan.isActive = true
verticalStackView.leftAnchor.constraint(equalTo: self.leftAnchor,constant: 0).isActive = true
verticalStackView.rightAnchor.constraint(equalTo: self.rightAnchor,constant: 0).isActive = true
verticalStackView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
verticalStackView.layoutMargins = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)
verticalStackView.isLayoutMarginsRelativeArrangement = true
}
convenience init(orientation: NSLayoutConstraint.Axis,labelsArray: [UIView]) {
self.init()
verticalStackView.axis = orientation
for label in labelsArray {
verticalStackView.addArrangedSubview(label)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Example Controller Class (This is a minimized version of the whole project):
class ViewController: UIViewController, UITableViewDelegate,UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
let viewWithStack = BoxView()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
tableView.delegate = self
tableView.dataSource = self
tableView.register(TableViewCell.self, forCellReuseIdentifier: "myCell")
tableView.rowHeight = UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: TableViewCell = tableView.dequeueReusableCell(withIdentifier: "myCell") as! TableViewCell
if (indexPath.row == 0) {
cell.setup(viewWithStack: self.viewWithStack)
} else {
cell.backgroundColor = UIColor.black
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
//return 500
if ( indexPath.row == 0) {
return UITableView.automaticDimension
} else {
return 40
}
}
}
EDIT I created a minimal project then I found that my problem is that my project implements heightForRow function which overrides UITableViewAutomaticDimension so that It gives wrong height for my component. I think I should look how to get height size of the component? because I can't delete heightForRow function which solves my problem.
Example Project Link https://github.com/emreond/tableviewWithStackView/tree/master/tableViewWithStackViewEx
Example project has ambitious layouts when you open view debugger. I think when I fix them, everything should be fine.
Here is a full example that should do what you want (this is what I mean by a minimal reproducible example):
Best way to examine this is to:
create a new project
create a new file, named TestTableViewController.swift
copy and paste the code below into that file (replace the default template code)
add a UITableViewController to the Storyboard
assign its Custom Class to TestTableViewController
embed it in a UINavigationController
set the UINavigationController as Is Initial View Controller
run the app
This is what you should see as the result:
I based the classes on what you had posted (removed unnecessary code, and I am assuming you have the other cells working as desired).
//
// TestTableViewController.swift
//
// Created by Don Mag on 10/21/19.
//
import UIKit
class SideBySideCell: UITableViewCell {
let horizStackView: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.alignment = .fill
v.distribution = .fillEqually
v.spacing = 10
v.translatesAutoresizingMaskIntoConstraints = false
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()
}
override func prepareForReuse() {
horizStackView.arrangedSubviews.forEach {
$0.removeFromSuperview()
}
}
func commonInit() -> Void {
contentView.backgroundColor = UIColor(white: 0.8, alpha: 1.0)
contentView.addSubview(horizStackView)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
horizStackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
horizStackView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
horizStackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
horizStackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
])
}
func addViewWithStack(_ v: ViewWithStack) -> Void {
horizStackView.addArrangedSubview(v)
}
}
class TestTableViewController: UITableViewController {
let sideBySideReuseID = "sbsID"
override func viewDidLoad() {
super.viewDidLoad()
// register custom SideBySide cell for reuse
tableView.register(SideBySideCell.self, forCellReuseIdentifier: sideBySideReuseID)
tableView.separatorStyle = .none
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: sideBySideReuseID, for: indexPath) as! SideBySideCell
let twoLabelView1 = TwoLabelView()
twoLabelView1.firstLabelText = "Text for first label on left-side."
twoLabelView1.secondLabelText = "10.765,00TL"
let twoLabelView2 = TwoLabelView()
twoLabelView2.firstLabelText = "Text for second-first label on left-side."
twoLabelView2.secondLabelText = "10.765,00TL"
let twoLabelView3 = TwoLabelView()
twoLabelView3.firstLabelText = "Text for the first label on right-side."
twoLabelView3.secondLabelText = "10.765,00TL"
let leftStackV = ViewWithStack(orientation: .vertical, labelsArray: [twoLabelView1, twoLabelView2])
let rightStackV = ViewWithStack(orientation: .vertical, labelsArray: [twoLabelView3])
cell.addViewWithStack(leftStackV)
cell.addViewWithStack(rightStackV)
return cell
}
// create ViewWithStack using just a simple label
let cell = tableView.dequeueReusableCell(withIdentifier: sideBySideReuseID, for: indexPath) as! SideBySideCell
let v = UILabel()
v.text = "This is row \(indexPath.row)"
let aStackV = ViewWithStack(orientation: .vertical, labelsArray: [v])
cell.addViewWithStack(aStackV)
return cell
}
}
#IBDesignable class TwoLabelView: UIView {
var topMargin: CGFloat = 0.0
var verticalSpacing: CGFloat = 3.0
var bottomMargin: CGFloat = 0.0
#IBInspectable var firstLabelText: String = "" { didSet { updateView() } }
#IBInspectable var secondLabelText: String = "" { didSet { updateView() } }
fileprivate var firstLabel: UILabel = {
let v = UILabel()
return v
}()
fileprivate var secondLabel: UILabel = {
let v = UILabel()
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
setUpView()
}
required public init?(coder: NSCoder) {
super.init(coder:coder)
setUpView()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setUpView()
}
func setUpView() {
firstLabel.font = UIFont.systemFont(ofSize: 18.0, weight: UIFont.Weight.bold)
firstLabel.numberOfLines = 0
secondLabel.font = UIFont.systemFont(ofSize: 13.0, weight: UIFont.Weight.regular)
secondLabel.numberOfLines = 1
addSubview(firstLabel)
addSubview(secondLabel)
// we're going to set the constraints
firstLabel .translatesAutoresizingMaskIntoConstraints = false
secondLabel.translatesAutoresizingMaskIntoConstraints = false
// Note: recommended to use Leading / Trailing rather than Left / Right
NSLayoutConstraint.activate([
// pin both labels' left-edges to left-edge of self
firstLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
secondLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
// pin both labels' right-edges to right-edge of self
firstLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
secondLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
// pin firstLabel to the top of self + topMargin (padding)
firstLabel.topAnchor.constraint(equalTo: topAnchor, constant: topMargin),
// pin top of secondLabel to bottom of firstLabel + verticalSpacing
secondLabel.topAnchor.constraint(equalTo: firstLabel.bottomAnchor, constant: verticalSpacing),
// pin bottom of self to >= (bottom of secondLabel + bottomMargin (padding))
bottomAnchor.constraint(greaterThanOrEqualTo: secondLabel.bottomAnchor, constant: bottomMargin),
])
}
func updateView() -> Void {
firstLabel.text = firstLabelText
secondLabel.text = secondLabelText
}
}
class ViewWithStack: UIView {
let verticalStackView: UIStackView = {
let s = UIStackView()
s.distribution = .fill
s.spacing = 10
s.axis = .vertical
s.translatesAutoresizingMaskIntoConstraints = false
return s
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = UIColor.white
self.layer.cornerRadius = 6.0
// self.layer.applySketchShadow(color: UIColor(red:0.56, green:0.56, blue:0.56, alpha:1), alpha: 0.2, x: 0, y: 0, blur: 10, spread: 0)
addSubview(verticalStackView)
NSLayoutConstraint.activate([
// constrain to all 4 sides
verticalStackView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
verticalStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
verticalStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
verticalStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
])
verticalStackView.layoutMargins = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)
verticalStackView.isLayoutMarginsRelativeArrangement = true
}
convenience init(orientation: NSLayoutConstraint.Axis, labelsArray: [UIView]) {
self.init()
verticalStackView.axis = orientation
for label in labelsArray {
verticalStackView.addArrangedSubview(label)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

How to access cell members (for animation) from scrollViewDidScroll method in UICollectionViewController?

How do I access members (an UIImage or UITextView) added to the view in a UICollectionViewCell from the scrollViewDidScroll method in the UICollectionViewController?
I would like to animate i.e. move the image and text at "different speed" while scrolling vertically to the next cell.
I understand that this can be done within the scrollViewDidScroll method but I don't know how to access the members.
the ViewController:
class OnboardingViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
**code here which I just can't figure out....**
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.backgroundColor = .white
collectionView?.register(PageCell.self, forCellWithReuseIdentifier: "cellId")
collectionView?.isPagingEnabled = true
collectionView.showsHorizontalScrollIndicator = false
// this method "creates" the UIPageControll and assigns
setupPageControl()
}
lazy var pageControl: UIPageControl = {
let pageControl = UIPageControl()
pageControl.currentPage = 0
pageControl.numberOfPages = data.count <--- data provided by a model from a plist - works perfectly
pageControl.currentPageIndicatorTintColor = .black
pageControl.pageIndicatorTintColor = .gray
pageControl.translatesAutoresizingMaskIntoConstraints = false
return pageControl
}()
private func setupPageControl() {
view.addSubview(pageControl)
NSLayoutConstraint.activate([
onboardingPageControl.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
onboardingPageControl.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
onboardingPageControl.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
])
}
all the other override methods are implemented in an extension and working fine i.e.
numberOfItemsInSection section: Int) -> Int {
return data.count
}
This is the PageCell:
class PageCell: UICollectionViewCell {
var myPage: MyModel? {
didSet {
guard let unwrappedPage = myPage else { return }
// the image in question:
myImage.image = UIImage(named: unwrappedPage.imageName)
myImage.translatesAutoresizingMaskIntoConstraints = false
myImage.contentMode = .scaleAspectFit
// the text in question
let attributedText = NSMutableAttributedString(string: unwrappedPage.title, attributes: [:])
attributedText.append(NSAttributedString(string: "\n\(unwrappedPage.description)", attributes: [:]))
myText.attributedText = attributedText
myText.translatesAutoresizingMaskIntoConstraints = false
myText.textColor = .black
myText.textAlignment = .center
myText.isEditable = false
myText.isScrollEnabled = false
myText.isSelectable = false
}
let myImage: UIImageView = {
let imageView = UIImageView()
return imageView
}()
let myText: UITextView = {
let textView = UITextView()
return textView
}()
fileprivate func setup() {
addSubview(myImage)
NSLayoutConstraint.activate([
myImage.safeAreaLayoutGuide.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 60),
myImage.centerXAnchor.constraint(equalTo: centerXAnchor),
myImage.leadingAnchor.constraint(equalTo: leadingAnchor),
myImage.trailingAnchor.constraint(equalTo: trailingAnchor),
myImage.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.4)
])
addSubview(myText)
NSLayoutConstraint.activate([
myText.topAnchor.constraint(equalTo: myImage.bottomAnchor, constant: 16),
myText.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
myText.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16)
])
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
You can achieve this by getting current visible cells of your collectionView, and it would be great if you access them in scrollViewDidEndDecelerating intead of scrollViewDidScroll.
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
for cell in collectionView.visibleCells {
//cell.imageView
//cell.txtView
// you can access both imageView and txtView here
let indexPath = collectionView.indexPath(for: cell)
print(indexPath) // this will give you indexPath as well to differentiate
}
}

Constraint Crash after Dismissing View Controller

I'm a bit new to Xcode and been trying to do things programatically. I have View Controller A, B, C, and D. I have a back button on C, and D. When going from D to C using self.dismiss it works fine, however when I go from C to B I am getting a crash that looks like it's a constraint issue and I have no idea why.
Again, the crash occurs when going from C to B. The error says no common ancestor for DropDownButton, but there is no DropDownButton on ViewController B, it exists on C the one I am trying to dismiss.
I would like to know more about how the view controllers dismissing and Auto Layout works, could someone point me in the right direction please?
"oneonone.DropDownButton:0x7fcfe9d30660'πŸ‡ΊπŸ‡Έ+1 βŒ„'.bottom"> because they have no common ancestor. Does the constraint or its anchors reference items in different view hierarchies? That's illegal. userInfo: (null)
2018-11-09 19:56:22.828322-0600 oneonone[62728:4835265] *** Terminating app due to uncaught exception 'NSGenericException', reason: 'Unable to activate constraint with anchors <NSLayoutYAxisAnchor
UPDATE TO QUESTIONS:
Here is View Controller C, included is the var, adding it to subview, and how I dismiss this view controller
lazy var countryCodes: DropDownButton = {
let button = DropDownButton(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
let us = flag(country: "US")
let br = flag(country: "BR")
let lightGray = UIColor(red: 240/255, green: 240/255, blue: 240/255, alpha: 1)
button.backgroundColor = lightGray
button.setTitle(us + "+1 \u{2304}", for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 20)
button.setTitleColor(UIColor.darkGray, for: .normal)
button.uiView.dropDownOptions = [us + "+1", br + "+55", "+33", "+17", "+19"]
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
[countryCodes].forEach{ view.addSubview($0) }
setupLayout()
}
func setupLayout(){
countryCodes.translatesAutoresizingMaskIntoConstraints = false
countryCodes.topAnchor.constraint(equalTo: instructionLabel.bottomAnchor, constant: 30).isActive = true
countryCodes.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: -77.5).isActive = true
countryCodes.widthAnchor.constraint(equalToConstant: 85).isActive = true // guarantees this width for stack
countryCodes.heightAnchor.constraint(equalToConstant: 40).isActive = true
}
#objc func buttonPressed(){
self.dismiss(animated: true, completion: nil)
}
Here is the code in view controller B that (creates or presents?) View Controller C
#objc func phoneAuthButtonPressed(){
let vc = phoneAuthViewController()
self.present(vc, animated: true, completion: nil)
}
UPDATE 2: ADDING THE CUSTOM CLASS
Here is the button code that I used as a custom class following a tutorial, I believe the problem lies in here
protocol dropDownProtocol {
func dropDownPressed(string: String)
}
class DropDownButton: UIButton, dropDownProtocol {
var uiView = DropDownView()
var height = NSLayoutConstraint()
var isOpen = false
func dropDownPressed(string: String) {
self.setTitle(string + " \u{2304}", for: .normal)
self.titleLabel?.font = UIFont.systemFont(ofSize: 18)
self.dismissDropDown()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.gray
uiView = DropDownView.init(frame: CGRect.init(x: 0, y: 0, width: 0, height: 0))
uiView.delegate = self
uiView.layer.zPosition = 1 // show in front of other labels
uiView.translatesAutoresizingMaskIntoConstraints = false
}
override func didMoveToSuperview() {
self.superview?.addSubview(uiView)
self.superview?.bringSubviewToFront(uiView)
uiView.topAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
uiView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
uiView.widthAnchor.constraint(equalTo: self.widthAnchor).isActive = true
height = uiView.heightAnchor.constraint(equalToConstant: 0)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { // animates drop down list
NSLayoutConstraint.deactivate([self.height])
if self.uiView.tableView.contentSize.height > 150 {
self.height.constant = 150
} else {
self.height.constant = self.uiView.tableView.contentSize.height
}
if isOpen == false {
isOpen = true
NSLayoutConstraint.activate([self.height])
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseInOut, animations: {
self.uiView.layoutIfNeeded()
self.uiView.center.y += self.uiView.frame.height / 2
}, completion: nil)
} else {
dismissDropDown()
}
}
func dismissDropDown(){
isOpen = false
self.height.constant = 0
NSLayoutConstraint.activate([self.height])
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseInOut, animations: {
self.uiView.center.y -= self.uiView.frame.height / 2
self.uiView.layoutIfNeeded()
}, completion: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class DropDownView: UIView, UITableViewDelegate, UITableViewDataSource {
var dropDownOptions = [String]()
var tableView = UITableView()
var delegate : dropDownProtocol!
let lightGray = UIColor(red: 240/255, green: 240/255, blue: 240/255, alpha: 1)
override init(frame: CGRect) {
super.init(frame: frame)
tableView.backgroundColor = lightGray
tableView.delegate = self
tableView.dataSource = self
tableView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(tableView) // can not come after constraints
tableView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dropDownOptions.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = dropDownOptions[indexPath.row]
cell.textLabel?.font = UIFont.systemFont(ofSize: 14)
cell.textLabel?.textColor = UIColor.darkGray
cell.backgroundColor = lightGray
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.delegate.dropDownPressed(string: dropDownOptions[indexPath.row])
self.tableView.deselectRow(at: indexPath, animated: true)
}
}
Replace your didMoveToSuperview function with this and it will work. This function also gets called when the view its removed from the superview and the superview will be nil and that's causing the crash.
override func didMoveToSuperview() {
if let superview = self.superview {
self.superview?.addSubview(dropView)
self.superview?.bringSubviewToFront(dropView)
dropView.topAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
dropView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
dropView.widthAnchor.constraint(equalTo: self.widthAnchor).isActive = true
height = dropView.heightAnchor.constraint(equalToConstant: 0)
}
}
As I can see, the error says that one of countryCodes buttons is located in the different view than instructionLabel. They should have the same parent if you want them to be constrained by each other.
Hi I think the problem occurs in the didMoveToSuperView() function, because it is called also when the view is removed from it's superview. so when you try to setup the anchor to something that does no more exist it crashes.
try something like this :
if let superview = self.superview {
superview.addSubview(uiView)
superview.bringSubviewToFront(uiView)
uiView.topAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
uiView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
uiView.widthAnchor.constraint(equalTo: self.widthAnchor).isActive = true
height = uiView.heightAnchor.constraint(equalToConstant: 0)
}

Resources