How to position a subView below a subView with dynamic height? - ios

I have the following UIView:
It is a UIView which contains three subviews. A regular UILabel (Hello World) at the top, a custom UIViewController (CategoryList) which contains a CollectionView with buttons (alpha,beta, ...) and another custom UIViewController with just a label (SALUT).
I do auto-layout programmatically and position SALUT (var sCtrl) below the CategoryList (var catList) with
sCtrl.view.topAnchor.constraint(equalTo: catList.view.bottomAnchor).isActive = true
This results in the picture above, where SALUT is not positioned below the category list as I would like it to be. I sort of understand why since when I set the constraint the buttons are not yet laid out properly in the CollectionView and thus the bottom anchor of the catList is not set.
In the CategoryList:UIVIewController I have the following in order to get the collection view to get the correct height.
override public func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
heightConstraint = collectionView.heightAnchor.constraint(equalToConstant: collectionView.collectionViewLayout.collectionViewContentSize.height)
self.view.addConstraint(heightConstraint)
}
This works but since viewDidLayoutSubviews() is called after the constraint is set on SALUT the SALUT position is wrong. How can I get it right in an easy way? (I guess I could make a controller for my main view and check there when subviews are laid out and then update subview positions but this seems overly complicated to me).
The full code for the main view is below so you can see the layout positioning code. If needed I can also provide the subviews code but I suppose it shouldn't be needed since they should be considered black boxes...
class MyView: UIView {
let label = UILabel()
var sCtrl : SubController
var catList : CategoryList
var topConstraint = NSLayoutConstraint()
override init(frame: CGRect) {
sCtrl = SubController()
catList = CategoryList()
super.init(frame: frame)
setup()
}
private func setup() {
self.backgroundColor = UIColor.green
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = UIColor.yellow
label.text = "Hello world"
label.textAlignment = .center
self.addSubview(label)
catList.view.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(catList.view)
sCtrl.view.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(sCtrl.view)
let margins = self.layoutMarginsGuide
label.leadingAnchor.constraint(equalTo: margins.leadingAnchor, constant: 0).isActive = true
label.trailingAnchor.constraint(equalTo: margins.trailingAnchor, constant: 0).isActive = true
label.topAnchor.constraint(equalTo: margins.topAnchor, constant: 0).isActive = true
label.heightAnchor.constraint(equalToConstant: 100.0).isActive=true
catList.view.leadingAnchor.constraint(equalTo: margins.leadingAnchor, constant: 0).isActive = true
catList.view.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 1.0).isActive = true
catList.view.widthAnchor.constraint(equalTo: margins.widthAnchor, multiplier: 1.0).isActive = true
catList.view.trailingAnchor.constraint(equalTo: margins.trailingAnchor, constant:0).isActive = true
sCtrl.view.topAnchor.constraint(equalTo: catList.view.bottomAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
let v = MyView(frame: CGRect(x: 0, y: 0, width: 400, height: 600))

Ok, I found the issue. I had missed to add the height constraint to the CategoryList's own view. Adding this it works just fine.
override public func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
heightConstraint = collectionView.heightAnchor.constraint(equalToConstant: collectionView.collectionViewLayout.collectionViewContentSize.height)
self.view.addConstraint(heightConstraint)
//THE MISSING LINE!!
self.view.heightAnchor.constraint(equalTo: self.collectionView.heightAnchor, constant: 0).isActive = true
}

Related

ios Swift: ScrollView with dynamic content programmatic layout

Need to create custom view, just 2 buttons and some content between. Problem is about create correct layout using scrollView and subviews with dynamic content.
For example, if there will be only one Label.
What is my mistake?
Now label isn't visible, and view looks like:
Here is code:
view inits this way:
let view = MyView(frame: .zero)
view.configure(with ...) //here configures label text
selv.view.addSubView(view)
public final class MyView: UIView {
private(set) var titleLabel: UILabel?
override public init(frame: CGRect) {
let closeButton = UIButton(type: .system)
closeButton.translatesAutoresizingMaskIntoConstraints = false
(button setup)
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.showsVerticalScrollIndicator = false
scrollView.alwaysBounceVertical = false
let contentLayoutGuide = scrollView.contentLayoutGuide
let titleLabel = UILabel()
titleLabel.translatesAutoresizingMaskIntoConstraints = false
(label's font and alignment setup)
let successButton = UIButton(type: .system)
successButton.translatesAutoresizingMaskIntoConstraints = false
(button setup)
super.init(frame: frame)
addSubview(closeButton)
addSubview(scrollView)
addSubview(successButton)
scrollView.addSubview(titleLabel)
self.textLabel = textLabel
let layoutGuide = UILayoutGuide()
addLayoutGuide(layoutGuide)
NSLayoutConstraint.activate([
layoutGuide.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 2),
trailingAnchor.constraint(equalToSystemSpacingAfter: layoutGuide.trailingAnchor, multiplier: 2),
layoutGuide.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 2),
bottomAnchor.constraint(equalToSystemSpacingBelow: layoutGuide.bottomAnchor, multiplier: 2),
closeButton.leadingAnchor.constraint(greaterThanOrEqualTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(greaterThanOrEqualTo: closeButton.trailingAnchor),
closeButton.centerXAnchor.constraint(equalTo: layoutGuide.centerXAnchor),
closeButton.topAnchor.constraint(equalTo: layoutGuide.topAnchor),
closeButton.heightAnchor.constraint(equalToConstant: 33),
scrollView.topAnchor.constraint(equalTo: closeButton.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: successButton.topAnchor),
scrollView.contentLayoutGuide.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
scrollView.contentLayoutGuide.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
successButton.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(equalTo: successButton.trailingAnchor),
successButton.heightAnchor.constraint(equalToConstant: 48),
layoutGuide.bottomAnchor.constraint(equalTo: successButton.bottomAnchor),
titleLabel.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 16),
titleLabel.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 16),
titleLabel.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -16),
titleLabel.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -16),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func configure(with viewModel: someViewModel) {
titleLabel?.text = viewModel.title
}
}
If I'll add scrollView frameLayoutGuide height:
scrollView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 150),
, then all looks as expected, but I need to resize this label and all MyView height depending on content.
A UIScrollView is designed to automatically allow scrolling when its content is larger than its frame.
By itself, a scroll view has NO intrinsic size. It doesn't matter how many subviews you add to it... if you don't do something to set its frame, its frame size will always be .zero.
If we want to get the scroll view's frame to grow in height based on its content we need to give it a height constraint when the content size changes.
If we want it to scroll when it has a lot of content, we also need to give it a maximum height.
So, if we want MyView height to be max of 1/2 the screen (view) height, we constrain its height (in the controller) like this:
myView.heightAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.heightAnchor, multiplier: 0.5)
and then constrain the scroll view height in MyView like this:
let svh = scrollView.heightAnchor.constraint(equalToConstant: scrollView.contentSize.height)
svh.priority = .required - 1
svh.isActive = true
Here is a modification to your code - lots of comments in the code so you should be able to follow.
First, an example controller:
class MVTestVC: UIViewController {
let myView = MyView()
let sampleStrings: [String] = [
"Short string.",
"This is a longer string which should wrap onto a couple lines.",
"Now let's use a really, really long string. This will make the label taller, but still not enough to require vertical scrolling.",
"We want to see what happens when we DO need scrolling.\n\nSo, let's use a long string, with some embedded newlines.\n\nThis will make the label tall enough that it would exceed one-half the screen height, so we can see that we do, in fact, get vertical scrolling.",
]
var strIndex: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .gray
myView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// 20-points on each side
myView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
myView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// centered vertically
myView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// max 1/2 screen (view) height
myView.heightAnchor.constraint(lessThanOrEqualTo: g.heightAnchor, multiplier: 0.5),
])
myView.backgroundColor = .white
myView.configure(with: sampleStrings[0])
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
strIndex += 1
myView.configure(with: sampleStrings[strIndex % sampleStrings.count])
}
}
and the modified MyView class:
public final class MyView: UIView {
private let titleLabel = UILabel()
private let scrollView = UIScrollView()
// this will be used to set the scroll view height
private var svh: NSLayoutConstraint!
override public init(frame: CGRect) {
super.init(frame: frame)
let closeButton = UIButton(type: .system)
closeButton.translatesAutoresizingMaskIntoConstraints = false
//(button setup)
closeButton.setTitle("X", for: [])
closeButton.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.showsVerticalScrollIndicator = false
scrollView.alwaysBounceVertical = false
titleLabel.translatesAutoresizingMaskIntoConstraints = false
//(label's font and alignment setup)
titleLabel.font = .systemFont(ofSize: 24.0, weight: .light)
titleLabel.numberOfLines = 0
let successButton = UIButton(type: .system)
successButton.translatesAutoresizingMaskIntoConstraints = false
//(button setup)
successButton.setTitle("Success", for: [])
successButton.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
addSubview(closeButton)
addSubview(scrollView)
addSubview(successButton)
scrollView.addSubview(titleLabel)
let layoutGuide = UILayoutGuide()
addLayoutGuide(layoutGuide)
let contentLayoutGuide = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
layoutGuide.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 2),
trailingAnchor.constraint(equalToSystemSpacingAfter: layoutGuide.trailingAnchor, multiplier: 2),
layoutGuide.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 2),
bottomAnchor.constraint(equalToSystemSpacingBelow: layoutGuide.bottomAnchor, multiplier: 2),
closeButton.leadingAnchor.constraint(greaterThanOrEqualTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(greaterThanOrEqualTo: closeButton.trailingAnchor),
closeButton.centerXAnchor.constraint(equalTo: layoutGuide.centerXAnchor),
closeButton.topAnchor.constraint(equalTo: layoutGuide.topAnchor),
closeButton.heightAnchor.constraint(equalToConstant: 33),
scrollView.topAnchor.constraint(equalTo: closeButton.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: successButton.topAnchor),
successButton.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(equalTo: successButton.trailingAnchor),
successButton.heightAnchor.constraint(equalToConstant: 48),
layoutGuide.bottomAnchor.constraint(equalTo: successButton.bottomAnchor),
// constrain the label to the scroll view's Content Layout Guide
titleLabel.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor, constant: 16),
titleLabel.leadingAnchor.constraint(equalTo: contentLayoutGuide.leadingAnchor, constant: 16),
titleLabel.trailingAnchor.constraint(equalTo: contentLayoutGuide.trailingAnchor, constant: -16),
titleLabel.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor, constant: -16),
// label needs a width anchor, otherwise we'll get horizontal scrolling
titleLabel.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -32),
])
layer.cornerRadius = 12
// so we can see the framing
scrollView.backgroundColor = .red
titleLabel.backgroundColor = .green
}
public override func layoutSubviews() {
super.layoutSubviews()
// we want to update the scroll view's height constraint when the text changes
if let c = svh {
c.isActive = false
}
// on initial layout, the scroll view's content size will still be zero
// so force another layout pass
if scrollView.contentSize.height == 0 {
scrollView.setNeedsLayout()
scrollView.layoutIfNeeded()
}
// constrain the scroll view's height to the height of its content
// but with a less-than-required priority so we can use a maximum height
svh = scrollView.heightAnchor.constraint(equalToConstant: scrollView.contentSize.height)
svh.priority = .required - 1
svh.isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//public func configure(with viewModel: someViewModel) {
// titleLabel.text = viewModel.title
//}
public func configure(with str: String) {
titleLabel.text = str
// force the scroll view to update its layout
scrollView.setNeedsLayout()
scrollView.layoutIfNeeded()
// force self to update its layout
self.setNeedsLayout()
self.layoutIfNeeded()
}
}
Each tap anywhere on the screen will cycle through a few sample strings to change the text in the label, giving us this:

Swift - animate linear diagram with minimal segment

I have a linear diagram view which basically is nothing but three views. They represent the percentage of certain asset in a portfolio. So, when I click a button, I call update(multipliers: [CGFloat]) and the animation starts. However, I ran into a few issues. Imagine if I have $10000 in stocks, $5000 in bonds and $1 in ETFs. The ETF view won't be visible at all because it's so small. But even in this case I want to show a 10px or something width view. How can I achieve this? Also, my debugger keeps warning me
Unable to simultaneously satisfy constraints. Probably at least one
of the constraints in the following list is one you don't want.
which is understandable, but I don't think is fixable because of multipliers and constants not matching the width.
So, how can I solve the issue?
import UIKit
class StripView: UIView {
var stocksConstraint = NSLayoutConstraint()
var bondsConstraint = NSLayoutConstraint()
var etfConstraint = NSLayoutConstraint()
let stocksView: UIView = {
let view = UIView()
view.backgroundColor = .lightGray
view.layer.cornerRadius = 4
return view
}()
let bondsView: UIView = {
let view = UIView()
view.backgroundColor = .lightGray
view.layer.cornerRadius = 4
return view
}()
let etfView: UIView = {
let view = UIView()
view.backgroundColor = .lightGray
view.layer.cornerRadius = 4
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
layer.cornerRadius = 4
layer.masksToBounds = true
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(multipliers: [CGFloat]) {
stocksView.removeConstraint(stocksConstraint)
stocksConstraint = stocksView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: multipliers[0])
stocksConstraint.isActive = true
bondsView.removeConstraint(bondsConstraint)
bondsConstraint = bondsView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: multipliers[1])
bondsConstraint.isActive = true
etfView.removeConstraint(etfConstraint)
etfConstraint = etfView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: multipliers[2])
etfConstraint.isActive = true
UIView.animate(withDuration: 1.0) {
self.layoutIfNeeded()
self.stocksView.backgroundColor = .systemRed
self.bondsView.backgroundColor = .systemGreen
self.etfView.backgroundColor = .systemBlue
}
}
func setupViews() {
let views = [stocksView, bondsView, etfView]
views.forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
stocksConstraint = stocksView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.33)
bondsConstraint = bondsView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.33)
etfConstraint = etfView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.33)
NSLayoutConstraint.activate([
stocksView.topAnchor.constraint(equalTo: topAnchor),
stocksView.bottomAnchor.constraint(equalTo: bottomAnchor),
stocksView.leadingAnchor.constraint(equalTo: leadingAnchor),
stocksConstraint,
bondsView.topAnchor.constraint(equalTo: topAnchor),
bondsView.bottomAnchor.constraint(equalTo: bottomAnchor),
bondsView.leadingAnchor.constraint(equalTo: stocksView.trailingAnchor, constant: 2),
bondsConstraint,
etfView.topAnchor.constraint(equalTo: topAnchor),
etfView.bottomAnchor.constraint(equalTo: bottomAnchor),
etfView.leadingAnchor.constraint(equalTo: bondsView.trailingAnchor, constant: 2),
etfConstraint
])
}
}

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;
}
}

Creating a reusable UIView subclass - running into sizing issues

I am trying to create a small reusable view for the "tags" at the bottom ("PC-12 Restricted" and "PC-24 Restricted").
This is the code I have, and mostly it's working.
class TagView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private let label = UILabel()
func setup() {
translatesAutoresizingMaskIntoConstraints = false
label.text = "Placeholder"
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .caption2)
addSubview(label)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
label.topAnchor.constraint(equalTo: topAnchor, constant: 4),
label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),
])
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.height / 2
}
func configure(tag: Tag) {
label.text = tag.label
backgroundColor = tag.backgroundColor
}
}
However, when I place it inside a UIStackView inside of a UITableViewCell, it moves outside of the stackview? Also, I added two of these TagViews, but they are showing on top of each other!
When I use the view debugger in Xcode, I get some exclamation marks with the following info:
Why is the position ambiguous? What am I missing? When I print the TagViews that I am adding to my view hierarchy, I do see that they have a frame with everything set to 0, but when I look in the view debugger, they do have a frame.
let tagViews = airport.tags.map { tag -> TagView in
let tagView = TagView()
tagView.configure(tag: tag)
return tagView
}
print(tagViews)
for view in tagViews {
tagsStackView.addSubview(view)
}
[<MyApp.TagView: 0x7f81ac449570; frame = (0 0; 0 0); layer = <CALayer: 0x600001afaba0>>, <MyApp.TagView: 0x7f81ac449c70; frame = (0 0; 0 0); layer = <CALayer: 0x600001afae60>>]
Edit:
If I add two TagViews to my the UIStackView inside of my UITableViewCell .xib file, it works fine:
But when I instantiate and add them programmatically, I have the layout problems as described.
Edit 2:
I have a very small test project so you can reproduce the problem: https://github.com/kevinrenskers/TagViewTest
Finally found the problem. And I feel stupid 🤦‍♂️
tagsStackView.addSubview(tagView)
should of course be
tagsStackView.addArrangedSubview(tagView)

UICollectionViewCell layout with SnapKit

I have a simple UICollectionViewCell which is full width and I'm using SnapKit to layout its subviews. I'm using SnapKit for my other view and it's working great but not within the collectionViewCell. This is the layout I'm trying to achieve:
collectionViewCell layout
The code for the collectionViewCell is:
lazy var imageView: UIImageView = {
let img = UIImageView(frame: .zero)
img.contentMode = UIViewContentMode.scaleAspectFit
img.backgroundColor = UIColor.lightGray
return img
}()
override init(frame: CGRect) {
super.init(frame: frame)
let imageWidth = CGFloat(frame.width * 0.80)
let imageHeight = CGFloat(imageWidth/1.77)
backgroundColor = UIColor.darkGray
imageView.frame.size.width = imageWidth
imageView.frame.size.height = imageHeight
contentView.addSubview(imageView)
imageView.snp.makeConstraints { (make) in
make.top.equalTo(20)
//make.right.equalTo(-20)
//make.right.equalTo(contentView).offset(-20)
//make.right.equalTo(contentView.snp.right).offset(-20)
//make.right.equalTo(contentView.snp.rightMargin).offset(-20)
//make.right.equalToSuperview().offset(-20)
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Without applying any constraints the imageView is displayed top left in the cell but applying any constraints makes the image disappear. Inspecting the collectionViewCell in debug view shows the imageView and constraints but the 'Size and horizontal position are ambiguous for UIImageView'.
I have also tried setting contentView.translatesAutoresizingMaskIntoConstraints = false amongst other things but the same results.
I'm using all code and no storyboard UI or layouts.
Thanks for any help!
You can't use both auto layout (SnapKit) and manual layout (set frame). Usually, using auto layout will cause manual layout to fail. However, your code of the constraints in snp closure is not complete. Whether using auto layout or manual layout, finally the view should get the position and size.
So, you can try like this:
imageView.snp.makeConstraints { make in
// set size
make.size.equalTo(CGSize(width: imageWidth, height: imageHeight))
// set position
make.top.equalTo(20)
make.centerX.equalTo(contentView)
}
I don't know much about SnapKit but it looks like you're missing a constraint. You need height, width, x and y. Works nicely with IOS 9 constraints.
let imageView: UIImageView = {
let view = UIImageView()
view.translatesAutoresizingMaskIntoConstraints = false
view.contentMode = .scaleAspectFit
view.backgroundColor = .lightGray
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
func setupViews() {
self.addSubview(imageView)
imageView.topAnchor.constraint(equalTo: self.topAnchor, constant: 10).isActive = true
imageView.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -10).isActive = true
imageView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 2/3, constant: -10).isActive = true
imageView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 1/2, constant: -10).isActive = true
}

Resources