I have a simple class that contains two labels and a line in a stack:
class TestView: UIView
{
let label_A = UILabel()
let label_B = UILabel()
override init(frame: CGRect) { super.init(frame: frame); setup() }
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder); setup() }
func setup()
{
let view_BG = UIView()
let view_LineH = UIView()
// Configure
view_BG.backgroundColor = .white
view_BG.layer.cornerRadius = 6
view_LineH.backgroundColor = .gray
label_A.numberOfLines = 0
label_A.backgroundColor = UIColor(red: 1, green: 0, blue: 0, alpha: 0.2)
label_A.textColor = .red
label_B.numberOfLines = 0
label_B.textColor = .blue
label_B.backgroundColor = UIColor(red: 0, green: 0, blue: 1, alpha: 0.2)
label_A.text = "TestA"
label_B.text = "Experiment with Swift standard library types and learn high-level concepts using visualizations and practical examples. Learn how the Swift standard library uses protocols and generics to express powerful constraints. Download the playground below to get started."
// Assemble
self.addSubview(view_BG)
view_BG.addSubview(label_A)
view_BG.addSubview(view_LineH)
view_BG.addSubview(label_B)
// Layout
view_BG.constrain_edges(to: self)
label_A.constrain_edges(to: view_BG, excludingEdge: .bottom)
label_B.constrain_edges(to: view_BG, excludingEdge: .top)
view_LineH.constrain_height(1)
view_LineH.constrain_left(to: view_BG)
view_LineH.constrain_right(to: view_BG)
view_LineH.constrain_topToBottom(of: label_A)
label_B.constrain_topToBottom(of: view_LineH)
}
}
When I call sizeThatFits, it just spits the height back at me:
let v = TestView()
let s = v.sizeThatFits(CGSize(width: 200, height: 10000))
// s is (200, 10000)
How can I calculate the desired height with a given width?
I believe you want .systemLayoutSizeFitting():
let tv = TestView()
let targSize = CGSize(width: 200, height: 10000)
let fitSize = tv.systemLayoutSizeFitting(targSize, withHorizontalFittingPriority: 1000, verticalFittingPriority: 1)
print(fitSize)
// prints "(200.0, 245.0)" in my playground
I don't have whatever you are using for .constrain_edges stuff, so here is the actual view class I'm running:
class TestView: UIView
{
let label_A = UILabel()
let label_B = UILabel()
override init(frame: CGRect) { super.init(frame: frame); setup() }
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder); setup() }
func setup()
{
let view_BG = UIView()
let view_LineH = UIView()
// Configure
view_BG.backgroundColor = .white
view_BG.layer.cornerRadius = 6
view_LineH.backgroundColor = .gray
label_A.numberOfLines = 0
label_A.backgroundColor = UIColor(red: 1, green: 0, blue: 0, alpha: 0.2)
label_A.textColor = .red
label_B.numberOfLines = 0
label_B.textColor = .blue
label_B.backgroundColor = UIColor(red: 0, green: 0, blue: 1, alpha: 0.2)
label_A.text = "TestA"
label_B.text = "Experiment with Swift standard library types and learn high-level concepts using visualizations and practical examples. Learn how the Swift standard library uses protocols and generics to express powerful constraints. Download the playground below to get started."
// Assemble
self.addSubview(view_BG)
view_BG.addSubview(label_A)
view_BG.addSubview(view_LineH)
view_BG.addSubview(label_B)
view_BG.translatesAutoresizingMaskIntoConstraints = false
label_A.translatesAutoresizingMaskIntoConstraints = false
label_B.translatesAutoresizingMaskIntoConstraints = false
view_LineH.translatesAutoresizingMaskIntoConstraints = false
// Layout
view_BG.topAnchor.constraint(equalTo: self.topAnchor, constant: 0.0).isActive = true
view_BG.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 0.0).isActive = true
view_BG.rightAnchor.constraint(equalTo: self.rightAnchor, constant: 0.0).isActive = true
view_BG.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0.0).isActive = true
label_A.topAnchor.constraint(equalTo: view_BG.topAnchor, constant: 0.0).isActive = true
label_A.leftAnchor.constraint(equalTo: view_BG.leftAnchor, constant: 0.0).isActive = true
label_A.rightAnchor.constraint(equalTo: view_BG.rightAnchor, constant: 0.0).isActive = true
label_B.leftAnchor.constraint(equalTo: view_BG.leftAnchor, constant: 0.0).isActive = true
label_B.rightAnchor.constraint(equalTo: view_BG.rightAnchor, constant: 0.0).isActive = true
label_B.bottomAnchor.constraint(equalTo: view_BG.bottomAnchor, constant: 0.0).isActive = true
view_LineH.leftAnchor.constraint(equalTo: view_BG.leftAnchor, constant: 0.0).isActive = true
view_LineH.rightAnchor.constraint(equalTo: view_BG.rightAnchor, constant: 0.0).isActive = true
view_LineH.topAnchor.constraint(equalTo: label_A.bottomAnchor, constant: 0.0).isActive = true
label_B.topAnchor.constraint(equalTo: view_LineH.bottomAnchor, constant: 0.0).isActive = true
view_LineH.heightAnchor.constraint(equalToConstant: 1.0).isActive = true
}
}
Related
I am trying to build my tableview with custom UITableViewCell but facing some issues in coinstraints. Following is my custom UITablviewCell class:
class MovieCell: UITableViewCell {
static let identifier = "MovieCell"
let cellMargin = 15.0
private lazy var containerView: UIView = {
let view = UIView()
view.backgroundColor = .lightGray
view.layer.cornerRadius = 5
view.translatesAutoresizingMaskIntoConstraints = false
view.clipsToBounds = true
return view
}()
private let movieNameLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.movieAppBoldFont(size: 15)
label.numberOfLines = 0
return label
}()
private let movieImageView: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
return imageView
}()
var movieCellViewModel : MoviewCellViewModel = MoviewCellViewModel(image: nil, name: "") {
didSet {
movieNameLabel.text = movieCellViewModel.name
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0)
layoutSubView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func layoutSubView() {
addSubview(containerView)
containerView.addSubview(movieNameLabel)
containerView.addSubview(movieImageView)
let marginGuide = containerView.layoutMarginsGuide
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
containerView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
containerView.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor, constant: -cellMargin),
movieImageView.leadingAnchor.constraint(equalTo: marginGuide.leadingAnchor, constant: 5),
movieImageView.topAnchor.constraint(equalTo: marginGuide.topAnchor, constant: 5),
movieImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5),
movieImageView.widthAnchor.constraint(equalToConstant: 50),
movieImageView.heightAnchor.constraint(equalToConstant: 50),
movieNameLabel.leadingAnchor.constraint(equalTo: marginGuide.trailingAnchor, constant: 5),
movieNameLabel.trailingAnchor.constraint(equalTo: marginGuide.trailingAnchor, constant: -5),
movieNameLabel.topAnchor.constraint(equalTo: marginGuide.topAnchor, constant: 5),
movieNameLabel.bottomAnchor.constraint(equalTo: marginGuide.bottomAnchor, constant: -5)
])
}
}
I get the following result with the above code:
I am trying to create a UIView and add content to it. I am adding it because I want to show some empty space as a separator in tableview.
Couple issues...
Your first line in layoutSubView() is:
addSubview(containerView)
where is needs to be:
contentView.addSubview(containerView)
Second, you have movieImageView constrained to contentView:
movieImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5),
where it should be constrained to marginGuide:
movieImageView.bottomAnchor.constraint(equalTo: marginGuide.bottomAnchor, constant: -5),
Third, you have movieNameLabel.leadingAnchor constrained to marginGuide.trailingAnchor:
movieNameLabel.leadingAnchor.constraint(equalTo: marginGuide.trailingAnchor, constant: 5),
where it should be constrained to movieImageView.trailingAnchor:
movieNameLabel.leadingAnchor.constraint(equalTo: movieImageView.trailingAnchor, constant: 5),
Making those changes will give you this (I set image view background to blue, and label background to cyan):
However, when you run the app, you'll see lots of Unable to simultaneously satisfy constraints. messages. This is common when using subviews with subviews in cells.
To get rid of the auto-layout complaints, we can give the containerView bottom anchor a less-than-required priority:
let bottomC = containerView.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor, constant: -cellMargin)
bottomC.priority = .required - 1
and then activate that in your constraints block:
//containerView.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor, constant: -cellMargin),
bottomC,
Here's your complete MovieCell class with those changes:
class MovieCell: UITableViewCell {
static let identifier = "MovieCell"
let cellMargin = 15.0
private lazy var containerView: UIView = {
let view = UIView()
view.backgroundColor = .lightGray
view.layer.cornerRadius = 5
view.translatesAutoresizingMaskIntoConstraints = false
view.clipsToBounds = true
return view
}()
private let movieNameLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .systemFont(ofSize: 15, weight: .bold) // UIFont.movieAppBoldFont(size: 15)
label.numberOfLines = 0
return label
}()
private let movieImageView: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
return imageView
}()
var movieCellViewModel : MoviewCellViewModel = MoviewCellViewModel(image: nil, name: "") {
didSet {
movieNameLabel.text = movieCellViewModel.name
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0)
layoutSubView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func layoutSubView() {
contentView.addSubview(containerView)
containerView.addSubview(movieNameLabel)
containerView.addSubview(movieImageView)
let marginGuide = containerView.layoutMarginsGuide
let bottomC = containerView.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor, constant: -cellMargin)
bottomC.priority = .required - 1
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
containerView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
// containerView.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor, constant: -cellMargin),
bottomC,
movieImageView.leadingAnchor.constraint(equalTo: marginGuide.leadingAnchor, constant: 5),
movieImageView.topAnchor.constraint(equalTo: marginGuide.topAnchor, constant: 5),
// movieImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5),
movieImageView.bottomAnchor.constraint(equalTo: marginGuide.bottomAnchor, constant: -5),
movieImageView.widthAnchor.constraint(equalToConstant: 50),
movieImageView.heightAnchor.constraint(equalToConstant: 50),
// movieNameLabel.leadingAnchor.constraint(equalTo: marginGuide.trailingAnchor, constant: 5),
movieNameLabel.leadingAnchor.constraint(equalTo: movieImageView.trailingAnchor, constant: 5),
movieNameLabel.trailingAnchor.constraint(equalTo: marginGuide.trailingAnchor, constant: -5),
movieNameLabel.topAnchor.constraint(equalTo: marginGuide.topAnchor, constant: 5),
movieNameLabel.bottomAnchor.constraint(equalTo: marginGuide.bottomAnchor, constant: -5)
])
// during dev, so we can easily see the frames
movieImageView.backgroundColor = .systemBlue
movieNameLabel.backgroundColor = .cyan
}
}
I have a subclass of UIButton and it have an initialiser that accept a name and boolean. I have a function to toggle the hide and show imageView, and my auto layout set to when imageView hidden the anchor move into another imageView. I use the content hugging priority programmatically in this. so here is my code, can you show me why my uiimageView not hiding.
// This is in my subclass of UIButton
let profileLbl = UILabel()
let badgeImageView = UIImageView()
let rightArrowImageView = UIImageView()
var isHiddenBadge = false
var visibleProfileTrailingConstraint: NSLayoutConstraint!
var hiddenProfileTrailingConstraint: NSLayoutConstraint!
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
configure()
}
init(name: String, isBadgeHidden: Bool = false) {
super.init(frame: .zero)
profileLbl.text = name
profileLbl.font = UIFont(name: "NunitoSans-SemiBold", size: 16)
profileLbl.textColor = #colorLiteral(red: 0.2, green: 0.2, blue: 0.2, alpha: 1)
isHiddenBadge = isBadgeHidden
toggleHide(badge: isHiddenBadge)
configure()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func configure() {
translatesAutoresizingMaskIntoConstraints = false
[profileLbl, badgeImageView, rightArrowImageView].forEach({ v in
v.translatesAutoresizingMaskIntoConstraints = false
addSubview(v)
})
visibleProfileTrailingConstraint = profileLbl.trailingAnchor.constraint(equalTo: badgeImageView.leadingAnchor, constant: -5)
hiddenProfileTrailingConstraint = profileLbl.trailingAnchor.constraint(equalTo: rightArrowImageView.leadingAnchor, constant: -5)
visibleProfileTrailingConstraint.priority = .defaultHigh
hiddenProfileTrailingConstraint.priority = .defaultLow
badgeImageView.image = #imageLiteral(resourceName: "warning_error 1")
rightArrowImageView.image = #imageLiteral(resourceName: "ic-arrow-right")
NSLayoutConstraint.activate([
profileLbl.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 24),
profileLbl.centerYAnchor.constraint(equalTo: centerYAnchor),
visibleProfileTrailingConstraint,
hiddenProfileTrailingConstraint,
badgeImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
badgeImageView.widthAnchor.constraint(equalToConstant: 24),
badgeImageView.heightAnchor.constraint(equalToConstant: 24),
rightArrowImageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24),
rightArrowImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
rightArrowImageView.widthAnchor.constraint(equalToConstant: 16),
rightArrowImageView.heightAnchor.constraint(equalToConstant: 16)
])
}
private func toggleHide(badge: Bool) {
if badge == false {
if badgeImageView.isHidden {
badgeImageView.isHidden = false
visibleProfileTrailingConstraint.priority = .defaultHigh
hiddenProfileTrailingConstraint.priority = .defaultLow
}
} else {
visibleProfileTrailingConstraint.priority = .defaultLow
hiddenProfileTrailingConstraint.priority = .defaultHigh
badgeImageView.isHidden = true
}
}
// I initialise it in my viewController
let infoBtn = GTProfileBtn(name: "Basic Info", isBadgeHidden: false)
// this is when I try to test it in my viewDidLoad
infoBtn.isHiddenBadge = true
Use following
var isHiddenBadge = false {
didSet {
toggleHide(badge: isHiddenBadge)
}
}
The problem is you not calling toggleHide after setting isHiddenBadge. The above code will solve the issue.
Following this article I was trying to get proportional sizing on StackViews working.
https://spin.atomicobject.com/2017/02/07/uistackviev-proportional-custom-uiviews/
The assumption was that by overriding intrinsicContentSize we can specify a new number and it will figure out the ratio of the sizes of subViews and resize the views accordingly.
When I repeat the implementation I am getting some odd behaviour. The ratio is preserved but the last item is stretched to take up the remaining space instead of the items being scaled across the entire width of the parent view (see image below).
Code:
class GuageSection: UIView {
var width: Double = 1.0
override var intrinsicContentSize: CGSize {
return CGSize(width: width, height: 1.0)
}
}
which is used like this
var guageWrapper = UIStackView()
guageWrapper.distribution = .fillProportionally
let guageSection = GuageSection()
guageSection.width = category.range // Currently Doubles ranging between 1.0 and 1.5
guageWrapper.addArrangedSubview(guageSection)
I have tried playing with the translateAutoResizingMaskInConstraints property and a few other things but nothing seems to change this behaviour.
If anyone has seen this behaviour before a good point in the right direction would be very much appreciated.
I don't know whether this is a "bug" or not, but... It appears UIStackView has an issue with .fillProportionally and its initial layout calculations.
If .spacing is 0 (zero), .fillProportionally seems to work as documented. If .spacing is non-Zero, we see issues.
So, try this... Initialize your stack view with spacing of 0, then:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guageWrapper.spacing = 2
}
You'll need a reference to guageWrapper, of course, so create it as a class-level var.
Edit:
I whipped up an example with the stack view as part of a custom UIView.
Using an "intrinsic widths" array of 1.0, 2.0, 1.0, 1.0, 1.5, here is the result:
Everything is done via code (no #IBOutlets needed), so you should be able to run this by adding a new view controller and setting its custom class to GuageTestViewController:
//
// GuageTestViewController.swift
//
// Created by Don Mag on 2/28/19.
//
import UIKit
class GuageSection: UIView {
let label: UILabel = {
let v = UILabel()
v.textAlignment = .center
v.numberOfLines = 0
v.font = UIFont.systemFont(ofSize: 14.0)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let colorView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
var width: Double = 1.0
override var intrinsicContentSize: CGSize {
return CGSize(width: width, height: 1.0)
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
self.addSubview(colorView)
self.addSubview(label)
NSLayoutConstraint.activate([
colorView.topAnchor.constraint(equalTo: self.topAnchor, constant: 0.0),
colorView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0.0),
colorView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0.0),
colorView.heightAnchor.constraint(equalToConstant: 10.0),
label.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0.0),
label.centerXAnchor.constraint(equalTo: self.centerXAnchor, constant: 0.0),
label.widthAnchor.constraint(equalTo: colorView.widthAnchor, constant: 0.0),
label.topAnchor.constraint(equalTo: colorView.bottomAnchor, constant: 2.0),
])
}
}
class GuageView: UIView {
var pStack = UIStackView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
self.backgroundColor = UIColor(red: 41.0 / 255.0, green: 59.0 / 255.0, blue: 78.0 / 255.0, alpha: 1.0)
pStack.translatesAutoresizingMaskIntoConstraints = false
pStack.axis = .horizontal
pStack.alignment = .fill
pStack.distribution = .fillProportionally
pStack.spacing = 0
addSubview(pStack)
let labels = [
"Low", "Ideal", "Pre-High", "High", "Very High"
]
let rgbVals = [
[252, 191, 127],
[ 79, 197, 140],
[252, 191, 127],
[249, 129, 131],
[217, 92, 98],
]
let widths = [
1.0, 2.0, 1.0, 1.0, 1.5
]
for i in 0..<labels.count {
let v = GuageSection()
v.translatesAutoresizingMaskIntoConstraints = false
v.label.text = labels[i]
v.width = widths[i]
let rgb = rgbVals[i].compactMap { CGFloat($0) / 255.0 }
v.colorView.backgroundColor = UIColor(red: rgb[0], green: rgb[1], blue: rgb[2], alpha: 1.0)
v.label.textColor = v.colorView.backgroundColor
pStack.addArrangedSubview(v)
}
// constrain the stack view 20-pts from top, leading and trailing, 8-pts from bottom
NSLayoutConstraint.activate([
pStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
pStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
pStack.topAnchor.constraint(equalTo: topAnchor, constant: 20.0),
pStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
// no height constraint ...
// let the guageSection view height determine the stack view height
// guageSection has 10-pt tall view and multi-line capable label
])
}
override func layoutSubviews() {
super.layoutSubviews()
pStack.spacing = 2
}
}
class GuageTestViewController: UIViewController {
var gView = GuageView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red: 31.0 / 255.0, green: 46.0 / 255.0, blue: 61.0 / 255.0, alpha: 1.0)
view.addSubview(gView)
gView.translatesAutoresizingMaskIntoConstraints = false
// constrain the view to leading and trailing, and 40-pts from the top
NSLayoutConstraint.activate([
gView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0.0),
gView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0.0),
gView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40.0),
// no height constraint ...
// let the GuageView's content determine the height
])
}
}
As you can see from the picture, despite the "masksToBounds" I see the layer under the botton x. How can I solve?
class StickerView: UIImageView {
let stickerIdentifier: String
let xButton = UIButton().then {
$0.setImage(Asset.delete_sticker.image.withRenderingMode(.alwaysOriginal), for: .normal)
$0.layer.cornerRadius = 30/2
$0.imageView?.contentMode = .scaleAspectFit
$0.layer.masksToBounds = true
$0.alpha = 1
$0.translatesAutoresizingMaskIntoConstraints = true
}
init(frame: CGRect, name: String) {
self.stickerIdentifier = "\(name.replacingOccurrences(of: ".png", with: ""))"
super.init(frame: frame)
setLayout()
}
func setLayout() {
self.addSubview(xButton)
self.bringSubviewToFront(xButton)
self.xButton.activate([
self.xButton.topAnchor.constraint(equalTo: self.topAnchor, constant: -10),
self.xButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 10),
self.xButton.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.15),
self.xButton.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.15),
])
}
I'm trying to create a keyboard accessory view programmatically. I've set up a container view and inside that I'm trying to set up a textfield, post button, and an emoji.
Here's an example of what I'm trying to make.
Click here to view the image.
Here's the code that I am working with. I think the problem is when I'm setting the constraints.
Couple of questions running through my mind are:
Do I need to set up constraints for the container view?
How do I add appropriate constraints to the textfield?
override var inputAccessoryView: UIView? {
get {
//Set up the container
let containerView = UIView()
containerView.backgroundColor = #colorLiteral(red: 0.9784782529, green: 0.9650371671, blue: 0.9372026324, alpha: 1)
containerView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: 60)
let textField = UITextField()
textField.placeholder = "Add a reframe..."
textField.isSecureTextEntry = false
textField.textAlignment = .left
textField.borderStyle = .none
textField.translatesAutoresizingMaskIntoConstraints = false
textField.translatesAutoresizingMaskIntoConstraints = false
textField.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
textField.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
textField.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
textField.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
textField.heightAnchor.constraint(equalToConstant: 50)
containerView.addSubview(textField)
return containerView
}
}
This is the error that I keep getting.
*** Terminating app due to uncaught exception 'NSGenericException', reason: 'Unable to activate constraint with anchors and because they have no common ancestor. Does the constraint or its anchors reference items in different view hierarchies? That's illegal.'
EDIT:
View Post Controller
lazy var containerView: CommentInputAccessoryView = {
let frame = CGRect(x: 0, y: 0, width: view.frame.width, height: 60)
let commentInputAccessoryView = CommentInputAccessoryView(frame: frame)
commentInputAccessoryView.delegate = self
return commentInputAccessoryView
}()
//Setting up the keyboard accessory view for comments.
override var inputAccessoryView: UIView? {
get {
return containerView
}
}
override var canBecomeFirstResponder: Bool {
return true
}
CommentInputAccessoryView
protocol CommentInputAccessoryViewDelegate {
func didSend(for comment: String)
}
class CommentInputAccessoryView: UIView {
var delegate: CommentInputAccessoryViewDelegate?
func clearCommentTextView() {
commentTextView.text = nil
showPlaceholderLabel()
}
let commentTextView: UITextView = {
let text = UITextView()
text.translatesAutoresizingMaskIntoConstraints = false
//text.placeholder = "Add a reframe..."
text.textAlignment = .left
text.backgroundColor = #colorLiteral(red: 0.9784782529, green: 0.9650371671, blue: 0.9372026324, alpha: 1)
text.layer.cornerRadius = 50/2
text.layer.masksToBounds = true
text.isScrollEnabled = false
text.font = UIFont.systemFont(ofSize: 16)
text.textContainerInset = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 64)
//text.borderStyle = .none
return text
}()
let placeholderLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "Add a response..."
label.textColor = .black
label.font = UIFont.systemFont(ofSize: 16)
return label
}()
func showPlaceholderLabel() {
placeholderLabel.isHidden = false
}
let sendButton: UIButton = {
let send = UIButton(type: .system)
//let sendButton = UIImageView(image: #imageLiteral(resourceName: "arrowUp"))
send.translatesAutoresizingMaskIntoConstraints = false
send.setTitle("Send", for: .normal)
send.setTitleColor(#colorLiteral(red: 0.2901960784, green: 0.3725490196, blue: 0.937254902, alpha: 1), for: .normal)
send.contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 10)
send.addTarget(self, action: #selector(handlePostComment), for: .touchUpInside)
return send
}()
let hugButton: UIButton = {
let hug = UIButton()
hug.translatesAutoresizingMaskIntoConstraints = false
hug.setTitle("🤗", for: .normal)
hug.backgroundColor = #colorLiteral(red: 0.9784782529, green: 0.9650371671, blue: 0.9372026324, alpha: 1)
hug.contentEdgeInsets = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
hug.layer.cornerRadius = 25
hug.layer.masksToBounds = true
return hug
}()
override init(frame: CGRect) {
super.init(frame: frame)
autoresizingMask = .flexibleHeight
addSubview(commentTextView)
addSubview(sendButton)
addSubview(hugButton)
addSubview(placeholderLabel)
if #available(iOS 11.0, *) {
commentTextView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -10).isActive = true
hugButton.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -10).isActive = true
}
else {
}
commentTextView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 8).isActive = true
commentTextView.topAnchor.constraint(equalTo: self.topAnchor, constant: 10).isActive = true
commentTextView.trailingAnchor.constraint(equalTo: hugButton.leadingAnchor, constant: 8)
placeholderLabel.leadingAnchor.constraint(equalTo: commentTextView.leadingAnchor, constant: 18).isActive = true
placeholderLabel.centerYAnchor.constraint(equalTo: self.commentTextView.centerYAnchor).isActive = true
sendButton.trailingAnchor.constraint(equalTo: self.commentTextView.trailingAnchor, constant: -10).isActive = true
sendButton.centerYAnchor.constraint(equalTo: self.commentTextView.bottomAnchor, constant: -22).isActive = true
hugButton.leadingAnchor.constraint(equalTo: self.commentTextView.trailingAnchor, constant: 10).isActive = true
hugButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -8).isActive = true
hugButton.widthAnchor.constraint(equalToConstant: 40)
//hugButton.centerYAnchor.constraint(equalTo: self.commentTextView.centerYAnchor).isActive = true
NotificationCenter.default.addObserver(self, selector: #selector(handleTextChanged), name: .UITextViewTextDidChange, object: nil)
}
override var intrinsicContentSize: CGSize {
return .zero
}
#objc func handleTextChanged() {
placeholderLabel.isHidden = !self.commentTextView.text.isEmpty
}
#objc func handlePostComment() {
guard let commentText = commentTextView.text else {return}
delegate?.didSend(for: commentText)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Here are some photos that might help for what is happening.
InputAccessoryView working:
Click here
TextView expansion:
Click here.
Before applying constraints view should be in view hierarchy or error which you got will be raised. To get rid of error just do containerView.addSubview(textField) after let textField = UITextField().
Regarding example image you posted, initial solution could be something like this
override var inputAccessoryView: UIView? {
get {
//Set up the container
let containerView = UIView()
containerView.backgroundColor = #colorLiteral(red: 0.9784782529, green: 0.9650371671, blue: 0.9372026324, alpha: 1)
containerView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: 60)
let textField = UITextField()
containerView.addSubview(textField)
textField.translatesAutoresizingMaskIntoConstraints = false
textField.placeholder = "Add a reframe..."
textField.textAlignment = .left
textField.backgroundColor = .white
textField.layer.cornerRadius = 50/2
textField.layer.masksToBounds = true
textField.borderStyle = .none
textField.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8).isActive = true
textField.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -5).isActive = true
textField.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 10).isActive = true
textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 15, height: textField.frame.height)) // adding left padding so it's not sticked to border
textField.leftViewMode = .always
let arrow = UIImageView(image: #imageLiteral(resourceName: "arrowUp"))
containerView.addSubview(arrow)
arrow.translatesAutoresizingMaskIntoConstraints = false
arrow.trailingAnchor.constraint(equalTo: textField.trailingAnchor, constant: -10).isActive = true
arrow.centerYAnchor.constraint(equalTo: textField.centerYAnchor).isActive = true
let button = UIButton()
containerView.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("🤗", for: .normal)
button.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
button.leadingAnchor.constraint(equalTo: textField.trailingAnchor, constant: 10).isActive = true
button.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8).isActive = true
button.centerYAnchor.constraint(equalTo: textField.centerYAnchor).isActive = true
// Negative values for constraints can be avoided if we change order of views when applying constrains
// f.e. instead of button.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8).isActive = true
// write containerView.trailingAnchor.constraint(equalTo: button.trailingAnchor, constant: 8).isActive = true
return containerView
}
}