How to set constraints so that a label fills the entire screen? - swift

I'm having trouble setting constraints to a label programmatically in Swift. I want the label to fill the entire screen. But I dont know how to do.
Thank you for your help.

class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let label = UILabel()
label.text = "Hello"
label.backgroundColor = UIColor.yellow
self.view.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
let width: NSLayoutConstraint
width = label.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 1)
let top: NSLayoutConstraint
top = label.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor)
let bottom: NSLayoutConstraint
bottom = label.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor)
width.isActive = true
bottom.isActive = true
top.isActive = true
}
}

You can use this extension for all of your views
extension UIView {
public func alignAllEdgesWithSuperview() {
guard let superview = self.superview else {
fatalError("add \(self) to a superview first.")
}
self.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
self.leadingAnchor.constraint(equalTo: superview.leadingAnchor),
self.trailingAnchor.constraint(equalTo: superview.trailingAnchor),
self.topAnchor.constraint(equalTo: superview.topAnchor),
self.bottomAnchor.constraint(equalTo: superview.bottomAnchor)
]
NSLayoutConstraint.activate(constraints)
}
}
and the usage
view.addSubview(someView)
someView.alignAllEdgesWithSuperview()

Related

UIScrollView constraints unexpected behaviour

I have a simple log in view implemented as follows :
import UIKit
class LoginViewController: UIViewController {
private var safeArea : UILayoutGuide!
private let scrollView : UIScrollView = {
let view = UIScrollView()
view.translatesAutoresizingMaskIntoConstraints = false
view.keyboardDismissMode = .onDrag
return view
}()
private let containerView : UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let logoView : UIImageView = {
let view = UIImageView()
view.translatesAutoresizingMaskIntoConstraints = false
view.contentMode = .scaleAspectFill
view.layer.cornerRadius = 8
view.image = UIImage(named: "logo")!
return view
}()
private let emailOrPhoneTextFieldView : UITextField = {
let view = UITextField()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.borderColor = UIColor.lightGray.cgColor
view.layer.borderWidth = 0.5
view.layer.cornerRadius = 10
view.placeholder = "Email or phone"
view.font = UIFont.systemFont(ofSize: 16, weight: .regular)
view.textColor = .black
view.autocapitalizationType = .none
view.tintColor = UIColor(named: "myColor")
view.backgroundColor = .systemGray
return view
}()
private let passwordTextFieldView : UITextField = {
let view = UITextField()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.borderColor = UIColor.lightGray.cgColor
view.layer.borderWidth = 0.5
view.layer.cornerRadius = 10
view.placeholder = "Password"
view.font = UIFont.systemFont(ofSize: 16, weight: .regular)
view.textColor = .black
view.autocapitalizationType = .none
view.tintColor = UIColor(named: "myColor")
view.isSecureTextEntry = true
view.backgroundColor = .systemGray
return view
}()
private let logInButtonView : UIButton = {
let view = UIButton()
view.setTitle("Log in", for: .normal)
view.setTitleColor(.white, for : .normal)
view.setBackgroundImage( UIImage(named: "blue_pixel")!, for: .normal)
view.layer.cornerRadius = 10
view.layer.masksToBounds = true
view.translatesAutoresizingMaskIntoConstraints = false
view.addTarget(self, action: #selector(logInButtonClickedHandler), for: .touchUpInside)
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
safeArea = view.layoutMarginsGuide
setupViews()
}
private func setupViews()
{
view.addSubview(scrollView)
containerView.addSubview(logoView)
containerView.addSubview(emailOrPhoneTextFieldView)
containerView.addSubview(passwordTextFieldView)
containerView.addSubview(logInButtonView)
scrollView.addSubview(containerView)
let constraints = [
scrollView.topAnchor.constraint(equalTo: safeArea.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor),
containerView.topAnchor.constraint(equalTo: scrollView.topAnchor),
containerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
containerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
containerView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
logoView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 120),
logoView.widthAnchor.constraint(equalToConstant: 100),
logoView.heightAnchor.constraint(equalToConstant: 100),
logoView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
emailOrPhoneTextFieldView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
emailOrPhoneTextFieldView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16),
emailOrPhoneTextFieldView.topAnchor.constraint(equalTo: logoView.bottomAnchor, constant: 120),
emailOrPhoneTextFieldView.heightAnchor.constraint(equalToConstant: 50),
passwordTextFieldView.topAnchor.constraint(equalTo: emailOrPhoneTextFieldView.bottomAnchor),
passwordTextFieldView.leadingAnchor.constraint(equalTo: emailOrPhoneTextFieldView.leadingAnchor),
passwordTextFieldView.heightAnchor.constraint(equalToConstant: 50),
passwordTextFieldView.trailingAnchor.constraint(equalTo: emailOrPhoneTextFieldView.trailingAnchor),
logInButtonView.topAnchor.constraint(equalTo: passwordTextFieldView.bottomAnchor, constant: 16),
logInButtonView.leadingAnchor.constraint(equalTo: passwordTextFieldView.leadingAnchor),
logInButtonView.trailingAnchor.constraint(equalTo: passwordTextFieldView.trailingAnchor),
logInButtonView.heightAnchor.constraint(equalToConstant: 50),
logInButtonView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
]
NSLayoutConstraint.activate(constraints)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillShow(notification:)),
name: UIResponder.keyboardWillShowNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillShow(notification:)),
name: UIResponder.keyboardWillHideNotification,
object: nil)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
}
#objc private func logInButtonClickedHandler() {
print("button pressed")
}
}
//MARK: Keyboard Notifications
private extension LoginViewController {
#objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
scrollView.contentInset.bottom = keyboardSize.height
scrollView.verticalScrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height, right: 0)
}
}
#objc func keyboardWillHide(notification: NSNotification) {
scrollView.contentInset.bottom = .zero
scrollView.verticalScrollIndicatorInsets = .zero
}
}
Everything is fine with the implementation but 2 things looks very strange for me and I guess I misunderstood smth
If I comment out
containerView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
I see that my container view does not fit the whole screen width (actually it's about 50% of it)
Why? I set trailing and leading constraints to scrollview, which is 100% of view width.
If I comment out
logInButtonView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
I don't get button click events and I'm not able to input anything inside textfields. What is the issue here?
From the Apple Docs:
Constraints between the edges or margins of the scroll view and its
content attach to the scroll view’s content area.
Constraints between the height, width, or centers attach to the scroll
view’s frame.
Hence you need the width constraint in order to make the contentView the full width of the ScrollView's frame.
As above, without that constraint the contentView only has constraints to the top/bottom edge of the scrollView this doesn't define its height and so you need to add full top-to-bottom constraints on the subviews of the contentView in order to define its height.
If you use the View Hierarchy Debugger you'll see the contentView has 0 height without that constraint (it just isn't clipping the content), hence why you can't tap on any controls.
It's worth giving the 'Working with Scroll Views' section of Apple Auto-Layout docs a read.

Is there a short way to assign all the anchors of a view equals to another view's all anchors

let's say I have a container view, which has an imageView and blurEffectView as its subView. In short, I apply a blurEffect on this image inside the container view.
I used anchors to configure the auto-layout programmatically without IB. And the anchors of imageView and blurredEffectView are completely the same as their container view.
I'm wondering is there a simple/quick way to do things like aView.allAnchors.equals(bView's allAnchors)? Then we could save a lot of repeating code.
- container
- imageView
- blurEffectView
let container: UIView = {
let aView = UIView()
aView.translatesAutoresizingMaskIntoConstraints = false
return aView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
navigationController?.navigationBar.prefersLargeTitles = true
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
configureLayout()
}
func configureLayout() {
let imageView = UIImageView(image: UIImage(named: "Wang Fei"))
// imageView.frame = container.bounds
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
container.addSubview(imageView)
let blurEffect = UIBlurEffect(style: .dark)
let blurredEffectView = UIVisualEffectView(effect: blurEffect)
blurredEffectView.translatesAutoresizingMaskIntoConstraints = false
// blurredEffectView.frame = imageView.bounds
container.addSubview(blurredEffectView)
view.addSubview(container)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
container.topAnchor.constraint(equalTo: view.topAnchor),
container.leadingAnchor.constraint(equalTo: g.leadingAnchor),
container.trailingAnchor.constraint(equalTo: g.trailingAnchor),
container.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.42),
imageView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
imageView.topAnchor.constraint(equalTo: container.topAnchor),
imageView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
blurredEffectView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
blurredEffectView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
blurredEffectView.topAnchor.constraint(equalTo: container.topAnchor),
blurredEffectView.bottomAnchor.constraint(equalTo: container.bottomAnchor)
])
}
There's no built-in shortcut. So most people write one. Or they adopt an existing framework that provides such shortcuts; SnapKit is a popular choice.
You can wrap the repeating code in a convenient extension like this -
import UIKit
extension UIView {
func setUpEdgeToEdge(in container: UIView, insets: UIEdgeInsets = .zero) {
NSLayoutConstraint.activate([
self.topAnchor.constraint(equalTo: container.topAnchor, constant: insets.top),
self.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: insets.left),
self.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: insets.bottom),
self.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -insets.right),
])
}
}
Now you can call it like -
imageView.setUpEdgeToEdge(in: container)
// OR
imageView.setUpEdgeToEdge(in: container, insets: UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16))
You can use a frame instead of a constraint. Also, in code setting constraint and view inside the viewDidLayoutSubviewsit's not a good way. Also, use lazy.
Here is the possible solution.
class TestViewController: UIViewController {
private lazy var container: UIView = {
let aView = UIView()
aView.translatesAutoresizingMaskIntoConstraints = false
return aView
}()
private lazy var imageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "Red"))
imageView.translatesAutoresizingMaskIntoConstraints = true
imageView.contentMode = .scaleAspectFit
return imageView
}()
private lazy var blurredEffectView: UIVisualEffectView = {
let blurEffect = UIBlurEffect(style: .dark)
let blurredEffectView = UIVisualEffectView(effect: blurEffect)
blurredEffectView.translatesAutoresizingMaskIntoConstraints = false
return blurredEffectView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
navigationController?.navigationBar.prefersLargeTitles = true
setViews()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.imageView.frame = container.bounds
self.blurredEffectView.frame = container.bounds
}
private func setViews() {
view.addSubview(container)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
container.topAnchor.constraint(equalTo: view.topAnchor),
container.leadingAnchor.constraint(equalTo: g.leadingAnchor),
container.trailingAnchor.constraint(equalTo: g.trailingAnchor),
container.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.42)])
container.addSubview(imageView)
container.addSubview(blurredEffectView)
}
}

Increase UIView Height Programmatically Based on Changing SubView Height

I have been unable to figure out how to solve this issue of mine. I tried to follow the answer from this post. How to change UIView height based on elements inside it
Like the post answer says to do, I have:
set autolayout constraints between UIContainerView top to the UITextView top and UIContainerView bottom to the UITextView bottom (#1)
set height constraint on the text view (#2) and change its constant when resizing the text view (#3)
I have to do this all programmatically. I first set the frame for the container view and give it a specified height. I'm not sure if that is okay too. I also add (#1) in viewDidLoad and am unsure if that's correct.
The text view is not able to increase height either with the current constraints (it is able to if I remove the topAnchor constraint but the container view still doesn't change size).
class ChatController: UICollectionViewController, UICollectionViewDelegateFlowLayout, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
lazy var containerView: UIView = {
let containerView = UIView()
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height * 0.075)
return containerView
}()
lazy var textView: UITextView = {
let textView = UITextView()
textView.text = "Enter message..."
textView.isScrollEnabled = false
textView.translatesAutoresizingMaskIntoConstraints = false
textView.delegate = self
return textView
}()
override func viewDidLoad() {
super.viewDidLoad()
...
textViewDidChange(self.textView)
addContainerSubViews()
(#1)
containerView.topAnchor.constraint(equalTo: self.textView.topAnchor, constant: -UIScreen.main.bounds.size.height * 0.075 * 0.2).isActive = true
containerView.bottomAnchor.constraint(equalTo: self.textView.bottomAnchor, constant: UIScreen.main.bounds.size.height * 0.075 * 0.2).isActive = true
}
func addContainerSubViews() {
let height = UIScreen.main.bounds.size.height
let width = UIScreen.main.bounds.size.width
let containerHeight = height * 0.075
...//constraints for imageView and sendButton...
containerView.addSubview(self.textView)
self.textView.leftAnchor.constraint(equalTo: imageView.rightAnchor, constant: width/20).isActive = true
self.textView.rightAnchor.constraint(equalTo: sendButton.leftAnchor, constant: -width/20).isActive = true
(#2)
self.textView.heightAnchor.constraint(equalToConstant: containerHeight * 0.6).isActive = true
}
override var inputAccessoryView: UIView? {
get {
return containerView
}
}
(#3)
func textViewDidChange(_ textView: UITextView) {
let size = CGSize(width: view.frame.width, height: .infinity)
let estimatedSize = textView.sizeThatFits(size)
textView.constraints.forEach { (constraint) in
if constraint.firstAttribute == .height {
constraint.constant = estimatedSize.height
}
}
}
You can do this all with auto-layout / constraints. Because a UITextView with scrolling disabled will "auto-size" its height based on the text, no need to calculate height and change constraint constant.
Here's an example -- it's from a previous answer, modified to include your image view and send button:
class ViewController: UIViewController {
let testLabel: InputLabel = InputLabel()
override func viewDidLoad() {
super.viewDidLoad()
let instructionLabel = UILabel()
instructionLabel.textAlignment = .center
instructionLabel.text = "Tap yellow label to edit..."
let centeringFrameView = UIView()
// label properties
let fnt: UIFont = .systemFont(ofSize: 32.0)
testLabel.isUserInteractionEnabled = true
testLabel.font = fnt
testLabel.adjustsFontSizeToFitWidth = true
testLabel.minimumScaleFactor = 0.25
testLabel.numberOfLines = 2
testLabel.setContentHuggingPriority(.required, for: .vertical)
let minLabelHeight = ceil(fnt.lineHeight)
// so we can see the frames
centeringFrameView.backgroundColor = .red
testLabel.backgroundColor = .yellow
[centeringFrameView, instructionLabel, testLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
view.addSubview(instructionLabel)
view.addSubview(centeringFrameView)
centeringFrameView.addSubview(testLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// instruction label centered at top
instructionLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
instructionLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// centeringFrameView 20-pts from instructionLabel bottom
centeringFrameView.topAnchor.constraint(equalTo: instructionLabel.bottomAnchor, constant: 20.0),
// Leading / Trailing with 20-pts "padding"
centeringFrameView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
centeringFrameView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// test label centered vertically in centeringFrameView
testLabel.centerYAnchor.constraint(equalTo: centeringFrameView.centerYAnchor, constant: 0.0),
// Leading / Trailing with 20-pts "padding"
testLabel.leadingAnchor.constraint(equalTo: centeringFrameView.leadingAnchor, constant: 20.0),
testLabel.trailingAnchor.constraint(equalTo: centeringFrameView.trailingAnchor, constant: -20.0),
// height will be zero if label has no text,
// so give it a min height of one line
testLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: minLabelHeight),
// centeringFrameView height = 3 * minLabelHeight
centeringFrameView.heightAnchor.constraint(equalToConstant: minLabelHeight * 3.0)
])
// to handle user input
testLabel.editCallBack = { [weak self] str in
guard let self = self else { return }
self.testLabel.text = str
}
testLabel.doneCallBack = { [weak self] in
guard let self = self else { return }
// do something when user taps done / enter
}
let t = UITapGestureRecognizer(target: self, action: #selector(self.labelTapped(_:)))
testLabel.addGestureRecognizer(t)
}
#objc func labelTapped(_ g: UITapGestureRecognizer) -> Void {
testLabel.becomeFirstResponder()
testLabel.inputContainerView.theTextView.text = testLabel.text
testLabel.inputContainerView.theTextView.becomeFirstResponder()
}
}
class InputLabel: UILabel {
var editCallBack: ((String) -> ())?
var doneCallBack: (() -> ())?
override var canBecomeFirstResponder: Bool {
return true
}
override var canResignFirstResponder: Bool {
return true
}
override var inputAccessoryView: UIView? {
get { return inputContainerView }
}
lazy var inputContainerView: CustomInputAccessoryView = {
let v = CustomInputAccessoryView()
v.editCallBack = { [weak self] str in
guard let self = self else { return }
self.editCallBack?(str)
}
v.doneCallBack = { [weak self] in
guard let self = self else { return }
self.resignFirstResponder()
}
return v
}()
}
class CustomInputAccessoryView: UIView, UITextViewDelegate {
var editCallBack: ((String) -> ())?
var doneCallBack: (() -> ())?
let theTextView: UITextView = {
let tv = UITextView()
tv.isScrollEnabled = false
tv.font = .systemFont(ofSize: 16)
tv.autocorrectionType = .no
tv.returnKeyType = .done
return tv
}()
let imgView: UIImageView = {
let v = UIImageView()
v.contentMode = .scaleAspectFit
v.clipsToBounds = true
return v
}()
let sendButton: UIButton = {
let v = UIButton()
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .lightGray
autoresizingMask = [.flexibleHeight, .flexibleWidth]
if let img = UIImage(named: "testImage") {
imgView.image = img
} else {
imgView.backgroundColor = .systemBlue
}
let largeConfig = UIImage.SymbolConfiguration(pointSize: 22, weight: .regular, scale: .large)
let buttonImg = UIImage(systemName: "paperplane.fill", withConfiguration: largeConfig)
sendButton.setImage(buttonImg, for: .normal)
[theTextView, imgView, sendButton].forEach { v in
addSubview(v)
v.translatesAutoresizingMaskIntoConstraints = false
}
// if we want to see the image view and button frames
//[imgView, sendButton].forEach { v in
// v.backgroundColor = .systemYellow
//}
NSLayoutConstraint.activate([
// constrain image view 40x40 with 8-pts leading
imgView.widthAnchor.constraint(equalToConstant: 40.0),
imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor),
imgView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
// constrain image view 40x40 with 8-pts trailing
sendButton.widthAnchor.constraint(equalToConstant: 40.0),
sendButton.heightAnchor.constraint(equalTo: sendButton.widthAnchor),
sendButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
// constrain text view with 10-pts from
// image view trailing
// send button leading
theTextView.leadingAnchor.constraint(equalTo: imgView.trailingAnchor, constant: 10),
theTextView.trailingAnchor.constraint(equalTo: sendButton.leadingAnchor, constant: -10),
// constrain image view and button
// centered vertically
// at least 8-pts top and bottom
imgView.centerYAnchor.constraint(equalTo: centerYAnchor),
sendButton.centerYAnchor.constraint(equalTo: centerYAnchor),
imgView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 8.0),
sendButton.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 8.0),
imgView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8.0),
sendButton.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8.0),
// constrain text view 8-pts top/bottom
theTextView.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
theTextView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
])
theTextView.delegate = self
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if (text == "\n") {
textView.resignFirstResponder()
doneCallBack?()
}
return true
}
func textViewDidChange(_ textView: UITextView) {
editCallBack?(textView.text ?? "")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var intrinsicContentSize: CGSize {
return .zero
}
}
Output:

UIScrollView not scrolling w/ programmatic autolayout constraints

I am trying to understand how to use a UIScrollView programmatically.
When I give my content a size that does not fit on screen though, it does not scroll.
final class ProfileView: UIView {
private var isIpad: Bool {
return UIDevice.current.userInterfaceIdiom == .pad
}
private lazy var headerImageView: UIImageView = {
let iv = UIImageView(frame: .zero)
iv.translatesAutoresizingMaskIntoConstraints = false
iv.heightAnchor.constraint(equalToConstant: 600).isActive = true
iv.backgroundColor = .purple
return iv
}()
private lazy var profileImageView: UIImageView = {
let iv = UIImageView(frame: .zero)
iv.translatesAutoresizingMaskIntoConstraints = false
[iv.heightAnchor, iv.widthAnchor].forEach { $0.constraint(equalToConstant: 170).isActive = true }
iv.layer.cornerRadius = 170 / 2
iv.layer.borderColor = .white
iv.layer.borderWidth = 3
iv.layer.masksToBounds = true
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
iv.backgroundColor = .red
return iv
}()
private(set) lazy var nameLabel: UILabel = {
let label = UILabel(frame: .zero)
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "Foo\nBar"
label.numberOfLines = 2
return label
}()
private lazy var contentScrollView: UIScrollView = {
let sv = UIScrollView(frame: .zero)
sv.translatesAutoresizingMaskIntoConstraints = false
return sv
}()
override init(frame: CGRect) {
super.init(frame: frame)
configureLayout()
}
required init?(coder: NSCoder) {
return nil
}
private func configureLayout() {
addSubview(contentScrollView)
[headerImageView, profileImageView, nameLabel].forEach { contentScrollView.addSubview($0) }
let compactConstraints = [
contentScrollView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
contentScrollView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor),
contentScrollView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor),
contentScrollView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor),
headerImageView.topAnchor.constraint(equalTo: topAnchor),
headerImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
headerImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
profileImageView.centerYAnchor.constraint(equalTo: headerImageView.bottomAnchor),
profileImageView.centerXAnchor.constraint(equalTo: centerXAnchor),
nameLabel.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 16),
nameLabel.centerXAnchor.constraint(equalTo: profileImageView.centerXAnchor),
]
NSLayoutConstraint.activate(compactConstraints)
}
}
This gives me the following -
The header image pushes the name label and avatar off screen and scrolling does not work.
I've read a bunch about giving the scroll view a huge offset so it scrolls, but that surely cannot be correct.
You need to create the constraints of all the subviews to the scrollView , add width constraint to the header img = profileview width and finally make bottom of label = bottom of the scrollview to make it infer it's height
let compactConstraints = [
contentScrollView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
contentScrollView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor),
contentScrollView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor),
contentScrollView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor),
headerImageView.topAnchor.constraint(equalTo:contentScrollView.topAnchor),
headerImageView.leadingAnchor.constraint(equalTo:contentScrollView.leadingAnchor),
headerImageView.trailingAnchor.constraint(equalTo:contentScrollView.trailingAnchor),
headerImageView.widthAnchor.constraint(equalTo: self.widthAnchor) // add this
profileImageView.centerYAnchor.constraint(equalTo: headerImageView.bottomAnchor),
profileImageView.centerXAnchor.constraint(equalTo: centerXAnchor),
nameLabel.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 16),
nameLabel.centerXAnchor.constraint(equalTo: profileImageView.centerXAnchor),
nameLabel.bottomAnchor.constraint(equalTo: contentScrollView.bottomAnchor) // and this
]

StackView: Overriding custom UIview intrinsicContent size produces very unexpected outcomes

After struggling with programmatic dynamic UI elements for the last few weeks I decided I would give UIstackView a try.
I want custom UIView classes to occupy the stackview with different heights based upon user Input I would remove, add views on the fly.
I found out that stackViews base their 'cell' height upon the UI element's intrinsic content size. However, UIViews do not have one. I searched far and wide and found out that I need to override the View's intrisicContentsize function with one where I can explicitly set the width and height.
However results are very unpredictable and I'm sure there is some little thing that I just do not know dat causes this weird behaviour. Since I'm new to the language and there are a LOT of gotcha's I'm just gonna paste the code here and hope you'll be able to spot what I'm doing wrong.
I've read the docs ofc, a lot of articles, they all point to that override funcion that does not seem to work form me.
This is my mainViewController class;
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
var bv = BackButtonView(frame: CGRect.zero, image: UIImage(named: "backArrow.png")!)
var redView : UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
return view
}();
var blueView : UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .blue
return view
}();
let stack : UIStackView = {
let stack = UIStackView();
stack.translatesAutoresizingMaskIntoConstraints = false;
stack.axis = .vertical
stack.distribution = .fillProportionally;
stack.spacing = 8
return stack;
}();
override func viewDidLoad() {
let view = UIView(frame: UIScreen.main.bounds)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
self.view = view
self.view.addSubview(stack)
createLayout()
bv.intrinsicContentSize
stack.addArrangedSubview(bv)
print(bv.frame)
print(bv.intrinsicContentSize)
stack.layoutIfNeeded()
stack.addArrangedSubview(redView)
stack.addArrangedSubview(blueView)
}
private func setConstraints(view: UIView) -> [NSLayoutConstraint] {
return [
view.heightAnchor.constraint(equalToConstant: 50),
]
}
private func createLayout() {
stack.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
stack.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
stack.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
stack.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
}
}
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = MyViewController()
Now here's my custom UIClass that is giving me all this trouble:
import UIKit
public class BackButtonView : UIView {
public var size = CGSize(width: 10, height: UIView.noIntrinsicMetric)
override open var intrinsicContentSize: CGSize {
return size
}
var button : UIButton;
public init(frame: CGRect, image: UIImage) {
button = UIButton()
super.init(frame : frame);
self.translatesAutoresizingMaskIntoConstraints = false;
self.backgroundColor = .black
self.addSubview(button)
setupButton(image: image);
print(button.intrinsicContentSize)
}
let backButtonTrailingPadding : CGFloat = -18
lazy var buttonConstraints = [
button.heightAnchor.constraint(equalToConstant: 50),
button.widthAnchor.constraint(equalToConstant: 50),
button.centerYAnchor.constraint(equalTo: self.centerYAnchor),
button.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: backButtonTrailingPadding),
]
private func setupButton(image: UIImage) {
self.button.translatesAutoresizingMaskIntoConstraints = false;
self.button.setImage(image, for: .normal);
self.button.contentMode = .scaleToFill
NSLayoutConstraint.activate(buttonConstraints)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
struct anchors {
var topAnchor = NSLayoutConstraint()
var bottomAnchor = NSLayoutConstraint()
var leadingAnchor = NSLayoutConstraint()
var trailingAnchor = NSLayoutConstraint()
}
}
You can see that the specified width in the size proprrty gets ignored.
Under the above conditions., this is the output
If I now change the my custom UIClass's intrisiContentSize height to anyting else, this happens:
public var size = CGSize(width: UIView.noIntrinsicMetric, height: 10)
override open var intrinsicContentSize: CGSize {
return size
}
result:
Please help me figure out what is and isn't going on
The final screen should look something like this: