How to center multiple UI elements within a parent UIView - ios

I'm trying to create these 6 UITextField centered within a UIVIew that I have centered and is 0.85 the width of self.view. I was able to get it working on one iPhone size however it was hardcoded and doesn't transform well on other iPhone sizes.
So now I'm trying to figure out the best way to properly center these 6 elements.
Here's what I currently have:
class FormView: UIViewController {
//INSTANTIATING VARIABLES
...
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
setupCodeFieldView()
}
private func setupCodeFieldView(){
codeFieldView.backgroundColor = .green
codeFieldView.translatesAutoresizingMaskIntoConstraints = false
parentSubview.addSubview(codeFieldView)
codeFieldView.heightAnchor.constraint(equalToConstant: 60).isActive = true
codeFieldView.topAnchor.constraint(equalTo: parentSubview.bottomAnchor, constant: 5).isActive = true
//SETTING WIDTH SIZE AND CENTERING PARENT VIEW
codeFieldView.widthAnchor.constraint(equalTo:view.widthAnchor, multiplier: 0.85).isActive = true
codeFieldView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
setupCodeFields()
}
fileprivate func setupCodeFields() {
var textFieldArr: [UITextField] = []
for index in 0...5{
let field: UITextField = UITextField()
field.returnKeyType = .next
field.setUnderline()
field.borderStyle = .none
field.keyboardType = .numberPad
field.backgroundColor = UIColor.white
field.tag = index
field.textAlignment = .center
let valueWidth = codeFieldView.bounds.size.width/6
field.placeholder = String(describing: valueWidth)
field.accessibilityIdentifier = "field" + String(index)
codeFieldView.addSubview(field)
field.layer.cornerRadius = 5.0
field.layer.borderWidth = 1.0
field.layer.borderColor = UIColor(red: 0.45, green: 0.46, blue: 0.50, alpha: 1.00).cgColor
field.translatesAutoresizingMaskIntoConstraints = false
//HERE IS HOW SET THE WIDTH OF THE BUTTON
field.widthAnchor.constraint(equalToConstant: floor(valueWidth)-5).isActive = true
field.heightAnchor.constraint(equalToConstant: 60).isActive = true
field.topAnchor.constraint(equalTo: codeFieldView.topAnchor).isActive = true
if index == 0 {
field.leftAnchor.constraint(equalTo:codeFieldView.leftAnchor).isActive = true
} else {
field.leftAnchor.constraint(equalTo: textFieldArr[index-1].rightAnchor, constant: 5).isActive = true
}
textFieldArr.append(field)
}
}
}
Here's what I currently have. You can see that the 6 elements' parent view is centered and that I'm struggling to have the 6 children UITextFields perfectly spaced across the highlighted green parent view.
Mockup of how I'd like my UI to look:

You can do this easily with a UIStackView
Here's a quick example (based on your code):
class FormView: UIViewController {
//INSTANTIATING VARIABLES
//...
let codeFieldView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .gray
setupCodeFieldView()
}
private func setupCodeFieldView() {
codeFieldView.backgroundColor = .green
codeFieldView.translatesAutoresizingMaskIntoConstraints = false
// not clear what you're doing with "parentSubview"
// so let's just add it to the root view
view.addSubview(codeFieldView)
// always respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// height of 60
codeFieldView.heightAnchor.constraint(equalToConstant: 60),
// 85% of the width
codeFieldView.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.85),
// centered vertically and horizontally
codeFieldView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
codeFieldView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
setupCodeFields()
}
fileprivate func setupCodeFields() {
// let's add a stack view to codeFieldView
let stackView = UIStackView()
stackView.spacing = 5
stackView.distribution = .fillEqually
stackView.translatesAutoresizingMaskIntoConstraints = false
codeFieldView.addSubview(stackView)
NSLayoutConstraint.activate([
// constrain stack view to all 4 sides of code field view
stackView.topAnchor.constraint(equalTo: codeFieldView.topAnchor),
stackView.leadingAnchor.constraint(equalTo: codeFieldView.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: codeFieldView.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: codeFieldView.bottomAnchor),
])
// now we add the text fields
for index in 0...5 {
let field: UITextField = UITextField()
field.returnKeyType = .next
//field.setUnderline()
field.borderStyle = .none
field.keyboardType = .numberPad
field.backgroundColor = UIColor.white
field.tag = index
field.textAlignment = .center
field.accessibilityIdentifier = "field" + String(index)
field.layer.cornerRadius = 5.0
field.layer.borderWidth = 1.0
field.layer.borderColor = UIColor(red: 0.45, green: 0.46, blue: 0.50, alpha: 1.00).cgColor
// add it to the stack view
stackView.addArrangedSubview(field)
}
}
}
The result:
Your question didn't indicate how you want the UI to look on a wider device, so here's how that looks when the phone is rotated:

Related

UIView width and height adjustment according to it subview programatically

I want to create something like this:
There's a white box under the buttons. If we are using SwiftUI logic, it's vertical padding : 5 and horizontal padding : 10, to create it with SwiftUI is pretty easy, but from what I have learned there is no padding and background color to a UIStackView and to create something like this, you need a UIView then add the stack view on top of the UIView.
This is what I have done so far:
//
// TransaksiViewController.swift
// HaselWiratama
//
// Created by Farhandika on 18/09/21.
// Copyright © 2021 Hasel.id. All rights reserved.
//
import UIKit
class TransaksiViewController: UIViewController {
let pesanButton: BigButton = {
let button = BigButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .blue
button.configure(viewModel: MyCustomBigButton(title: "Phone",
imageName: "house", isSystemImage: false))
return button
}()
let ambulanceButton: BigButton = {
let button = BigButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .blue
button.configure(viewModel: MyCustomBigButton(title: "Phone",
imageName: "house", isSystemImage: false))
return button
}()
let topStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.distribution = .fillEqually
stackView.spacing = 10
return stackView
}()
let uiView = UIView()
func configureUIView() {
//Configure the stackview
topStackView.addArrangedSubview(pesanButton)
topStackView.addArrangedSubview(ambulanceButton)
// add stack to UIView
uiView.addSubview(topStackView)
NSLayoutConstraint.activate([
topStackView.heightAnchor.constraint(equalToConstant: 150),
topStackView.centerYAnchor.constraint(equalTo: uiView.centerYAnchor),
topStackView.centerXAnchor.constraint(equalTo: uiView.centerXAnchor)
])
uiView.translatesAutoresizingMaskIntoConstraints = false
uiView.backgroundColor = .purple
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .cyan
view.addSubview(uiView)
configureUIView()
NSLayoutConstraint.activate([
uiView.widthAnchor.constraint(equalToConstant: 500),
uiView.heightAnchor.constraint(equalToConstant: 400),
uiView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
uiView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
}
/* Ignore the button width and height because I have not add the constraint yet */
The result:
As you can see, the width and height of the UIView is not relative to its child view.
How do I emulate the same padding horizontal 10 and vertical 5 in UIView?
(Basically a progressive or responsive width and height of a UIView.)
Since you have mentioned that you used constraints, see the following code. It reflect a UIViewController with what you need:
class ViewController: UIViewController {
// Horizontal Stackview
lazy var stack: UIStackView = {
let view = UIStackView()
view.translatesAutoresizingMaskIntoConstraints = false
view.axis = .horizontal
view.spacing = 10 // Inter-item space
view.backgroundColor = .white
view.distribution = .fillEqually // Setting distribution to fill equally
return view
}()
// Button 1
lazy var button1: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Button 1", for: .normal)
button.backgroundColor = .red
return button
}()
// Button 2
lazy var button2: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Button 2", for: .normal)
button.backgroundColor = .blue
return button
}()
// View that holds the stackview
lazy var stackHolder: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .gray
view.addSubview(stackHolder)
stackHolder.addSubview(stack)
stack.addArrangedSubview(button1)
stack.addArrangedSubview(button2)
//Setting layout constraints
NSLayoutConstraint.activate([
stackHolder.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stackHolder.centerYAnchor.constraint(equalTo: view.centerYAnchor),
// Setting a width and height of the stack so that the `stackHolder` adjust relatively
stack.widthAnchor.constraint(equalToConstant: 250),
stack.heightAnchor.constraint(equalToConstant: 80),
// Setting the constraints with `constant` values for padding
stack.leadingAnchor.constraint(equalTo: stackHolder.leadingAnchor, constant: 5),
stack.trailingAnchor.constraint(equalTo: stackHolder.trailingAnchor, constant: -5),
stack.topAnchor.constraint(equalTo: stackHolder.topAnchor, constant: 10),
stack.bottomAnchor.constraint(equalTo: stackHolder.bottomAnchor, constant: -10),
])
}
}
This will output:

How to programmatically align elements in a Stackview using SnapKit?

I'm trying to achieve a specific design inside a custom reusable view.
(Something like this: )
So I pass an URL to retrive the image and I pass a String to add below.
Firstly, I want the entire view to be the width of the elements (if the text is long then the entire view will be wider), I don't know how to do that, the view seems to be the entire width of the screen.
Secondly, I want the items to be centered horizontally and vertically, what I tried does not work.
Here is my current code :
func initLayout() {
stackView.axis = NSLayoutConstraint.Axis.vertical
stackView.distribution = UIStackView.Distribution.fillEqually
stackView.alignment = UIStackView.Alignment.center
stackView.spacing = 10.0
stackView.addArrangedSubview(imageView)
stackView.addArrangedSubview(textContainer)
stackView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(stackView)
self.snp.makeConstraints { (make) -> Void in
make.height.equalTo(70)
}
self.stackView.snp.makeConstraints{ (make) -> Void in
make.edges.equalTo(self)
}
}
And it results in something like this:
As you can (or cannot) see, the view is centered in the middle of the screen, which is not what I want. The view should be the width of the text and everything centered inside this particular width, then I add it inside my VC and place it so it's leading.
if I understand well the is your constraint without Snapkit:
Set your objects under your class controller declaration:
let myImageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleToFill
iv.clipsToBounds = true
iv.backgroundColor = .red
iv.translatesAutoresizingMaskIntoConstraints = false
return iv
}()
let myLabel: UILabel = {
let label = UILabel()
label.text = "Débats"
label.textColor = .white
label.textAlignment = .center
label.font = .systemFont(ofSize: 30, weight: .semibold) // set your font size here
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
Now in viewDidLoad set parameters and constraints:
myImageView.image = UIImage(named: "profilo") // set here your image
let myWidth = myLabel.intrinsicContentSize.width // This reveal only text width in label
view.addSubview(myLabel)
myLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5).isActive = true
myLabel.heightAnchor.constraint(equalToConstant: 50).isActive = true
myLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 25).isActive = true
myLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
view.addSubview(myImageView)
myImageView.widthAnchor.constraint(equalToConstant: myWidth).isActive = true
myImageView.heightAnchor.constraint(equalTo: myImageView.widthAnchor).isActive = true
myImageView.bottomAnchor.constraint(equalTo: myLabel.topAnchor).isActive = true
myImageView.centerXAnchor.constraint(equalTo: myLabel.centerXAnchor).isActive = true
This is the result:
Intere code:
class Aiutotipo: UIViewController {
let myImageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleToFill
iv.clipsToBounds = true
iv.backgroundColor = .red
iv.translatesAutoresizingMaskIntoConstraints = false
return iv
}()
let myLabel: UILabel = {
let label = UILabel()
label.text = "Débats"
label.textColor = .white
label.textAlignment = .center
label.font = .systemFont(ofSize: 30, weight: .semibold)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
myImageView.image = UIImage(named: "profilo") // set here your image
let myWidth = myLabel.intrinsicContentSize.width // This reveal only text width in label
view.addSubview(myLabel)
myLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5).isActive = true
myLabel.heightAnchor.constraint(equalToConstant: 50).isActive = true
myLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 25).isActive = true
myLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
view.addSubview(myImageView)
myImageView.widthAnchor.constraint(equalToConstant: myWidth).isActive = true
myImageView.heightAnchor.constraint(equalTo: myImageView.widthAnchor).isActive = true
myImageView.bottomAnchor.constraint(equalTo: myLabel.topAnchor).isActive = true
myImageView.centerXAnchor.constraint(equalTo: myLabel.centerXAnchor).isActive = true
}
}

Vertical ScrollView not scrolling (No Storyboard)

I need help creating a Scroll View without Storyboards. Here is my code for setting up the Scroll View; I'm not setting a contentSize of the Scroll View because I'd like the scroll view content size to be dynamic, dependent on the amount of text in the TextView. What I did instead, is I tried adding a 'contentView' to the Scroll View and added all my UI elements into the contentView. Any help would be appreciated.
import Foundation
import UIKit
import UITextView_Placeholder
class ComposerVC: UIViewController {
private var scrollView: UIScrollView = {
let scrollView = UIScrollView(frame: UIScreen.main.bounds)
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
private var contentView: UIView = {
let content = UIView()
content.translatesAutoresizingMaskIntoConstraints = false
return content
}()
private var title: UITextView = {
let title = UITextView()
title.translatesAutoresizingMaskIntoConstraints = false
title.placeholder = "Untitled"
title.textColor = UIColor(hexString: "#50E3C2")
title.font = UIFont(name: "Rubik-BoldItalic", size: 32)
title.backgroundColor = .clear
title.isScrollEnabled = false
return title
}()
private var divider: UIView = {
let divider = UIView()
divider.translatesAutoresizingMaskIntoConstraints = false
divider.backgroundColor = UIColor(hexString: "#50E3C2")
return divider
}()
private var content: UITextView = {
let title = UITextView()
title.translatesAutoresizingMaskIntoConstraints = false
title.placeholder = "Begin writing here..."
title.textColor = .white
title.font = UIFont(name: "Avenir-Book", size: 15)
title.backgroundColor = .clear
title.isScrollEnabled = false
return title
}()
override func viewDidLoad() {
setupUI()
setupUIConstraints()
title.delegate = self
}
private func setupUI() {
view.backgroundColor = UIColor(hexString: "#131415")
view.addSubview(scrollView)
scrollView.addSubview(contentView)
contentView.addSubview(title)
contentView.addSubview(divider)
contentView.addSubview(content)
}
private func setupUIConstraints() {
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
contentView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
title.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 95).isActive = true
title.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 35).isActive = true
title.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -35).isActive = true
divider.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 15).isActive = true
divider.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
divider.heightAnchor.constraint(equalToConstant: 1).isActive = true
divider.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.8).isActive = true
content.topAnchor.constraint(equalTo: divider.bottomAnchor, constant: 15).isActive = true
content.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 35).isActive = true
content.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -35).isActive = true
}
}
extension ComposerVC: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
let fixedWidth = textView.frame.size.width
let newSize = textView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
textView.frame.size = CGSize(width: max(newSize.width, fixedWidth), height: newSize.height)
}
}
A couple tips:
Don't use existing names for variables... with your code as-is, private var title: UITextView causes problems (title is already a view controller property).
Use var names that imply the object... e.g. titleTextView and contentTextView instead of title and content
During development - particularly when you're working on layout - give your UI elements contrasting background colors so you can easily see their frames at runtime.
When using code-created views, set .clipsToBounds = true ... if you don't see any subviews you've added, you know the frame / constraints are missing something.
I don't have your UITextView_Placeholder import, but that shouldn't affect anything here...
So, first, change your viewDidLoad() to this:
override func viewDidLoad() {
setupUI()
setupUIConstraints()
titleTextView.delegate = self
// contrasting colors during development
scrollView.backgroundColor = .red
titleTextView.backgroundColor = .yellow
contentTextView.backgroundColor = .green
divider.backgroundColor = .blue
contentView.backgroundColor = .cyan
}
When you run it, you should see (scroll view background is red, and this is without the placeholder stuff):
It looks correct, except there's no cyan-colored contentView.
Now, clip the subviews of contentView:
private var contentView: UIView = {
let content = UIView()
content.translatesAutoresizingMaskIntoConstraints = false
// add this line
content.clipsToBounds = true
return content
}()
The result:
Where did everything go? Well, we didn't see the cyan contentView and now we don't see any of its subviews ... If we use Debug View Hierarchy we can find out contentView has a Height of Zero.
Fix that by constraining the bottom of the second text view in setupUIConstraints() (I've renamed it to contentTextView instead of just content):
contentTextView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -95).isActive = true
and we get:
Now the Height of the cyan contentView is controlled by correctly setup constraints of its subviews.
As a side note: with constraints setup properly, and scrolling disabled for the text views, you do not need your:
extension ComposerVC: UITextViewDelegate {
//func textViewDidChange(_ textView: UITextView) {
//...
//}
}
The text view will automatically size itself to its text:
Here's the complete edited code:
class ComposerVC: UIViewController {
private var scrollView: UIScrollView = {
let scrollView = UIScrollView(frame: UIScreen.main.bounds)
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
private var contentView: UIView = {
let content = UIView()
content.translatesAutoresizingMaskIntoConstraints = false
// add this line so we know if the constraints are set correctly
content.clipsToBounds = true
return content
}()
private var titleTextView: UITextView = {
let title = UITextView()
title.translatesAutoresizingMaskIntoConstraints = false
// title.placeholder = "Untitled"
title.textColor = UIColor(hexString: "#50E3C2")
title.font = UIFont(name: "Rubik-BoldItalic", size: 32)
title.backgroundColor = .clear
title.isScrollEnabled = false
return title
}()
private var divider: UIView = {
let divider = UIView()
divider.translatesAutoresizingMaskIntoConstraints = false
divider.backgroundColor = UIColor(hexString: "#50E3C2")
return divider
}()
private var contentTextView: UITextView = {
let title = UITextView()
title.translatesAutoresizingMaskIntoConstraints = false
// title.placeholder = "Begin writing here..."
title.textColor = .white
title.font = UIFont(name: "Avenir-Book", size: 15)
title.backgroundColor = .clear
title.isScrollEnabled = false
return title
}()
override func viewDidLoad() {
setupUI()
setupUIConstraints()
titleTextView.delegate = self
// contrasting colors during development
scrollView.backgroundColor = .red
titleTextView.backgroundColor = .yellow
contentTextView.backgroundColor = .green
divider.backgroundColor = .blue
contentView.backgroundColor = .cyan
}
private func setupUI() {
view.backgroundColor = UIColor(hexString: "#131415")
view.addSubview(scrollView)
scrollView.addSubview(contentView)
contentView.addSubview(titleTextView)
contentView.addSubview(divider)
contentView.addSubview(contentTextView)
}
private func setupUIConstraints() {
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
contentView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
titleTextView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 95).isActive = true
titleTextView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 35).isActive = true
titleTextView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -35).isActive = true
divider.topAnchor.constraint(equalTo: titleTextView.bottomAnchor, constant: 15).isActive = true
divider.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
divider.heightAnchor.constraint(equalToConstant: 1).isActive = true
divider.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.8).isActive = true
contentTextView.topAnchor.constraint(equalTo: divider.bottomAnchor, constant: 15).isActive = true
contentTextView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 35).isActive = true
contentTextView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -35).isActive = true
contentTextView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -95).isActive = true
}
}
extension ComposerVC: UITextViewDelegate {
// func textViewDidChange(_ textView: UITextView) {
// let fixedWidth = textView.frame.size.width
// let newSize = textView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
// textView.frame.size = CGSize(width: max(newSize.width, fixedWidth), height: newSize.height)
// }
}
Assuming you are using iOS 11+, Your contentView should have its anchors constrained to the contentLayoutGuide of the scrollView. like this:
contentView
.leadingAnchor
.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor).isActive = true
contentView
.topAnchor
.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor).isActive = true
contentView
.trailingAnchor
.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor).isActive = true
contentView
.bottomAnchor
.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor).isActive = true
Also, its width should be constrained to the scrollView's frameLayoutGuide and not the view's width, like this:
contentView
.widthAnchor
.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor).isActive = true
That should make the scrollView detect the content size to fit.

UIButton in a view with animation not detecting touch

I'm following a tutorial to create an interactive popup animation (http://www.swiftkickmobile.com/building-better-app-animations-swift-uiviewpropertyanimator/), and now would like to add buttons to the popup rather than have static text as in the tutorial.
The animation works fine, but the buttons are not detecting touch. Touching the button causes the animation to reverse, instead of printing my "test" statement.
From research, it looks to either be an issue with view hierarchies, the animation disabling the button touch, or layout/constraint issues with the button. I've tried addressing the above issues, but nothing has worked, was hoping someone might be able to help?
I've left out the code pertaining to the animation since I think the issue is to do with layout (and the animation seems to be working fine) - but it's still a lot; apologies in advance for the large amount of code.
class ViewController: UIViewController {
private let popupOffset: CGFloat = 440
private lazy var contentImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = #imageLiteral(resourceName: "Background")
return imageView
}()
private lazy var overlayView: UIView = {
let view = UIView()
view.backgroundColor = .black
view.alpha = 0
return view
}()
private lazy var popupView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner]
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOpacity = 0.1
view.layer.shadowRadius = 10
return view
}()
private lazy var closedTitleLabel: UILabel = {
let label = UILabel()
label.text = "Hello"
label.font = UIFont.systemFont(ofSize: 16, weight: UIFont.Weight.medium)
label.textColor = UIColor.darkGray
label.textAlignment = .center
return label
}()
private lazy var openTitleLabel: UILabel = {
let label = UILabel()
label.text = "Which door will you choose?"
label.font = UIFont.systemFont(ofSize: 16, weight: UIFont.Weight.medium)
label.textColor = UIColor.darkGray
label.textAlignment = .center
label.alpha = 0
label.transform = CGAffineTransform(scaleX: 1.6, y: 1.6).concatenating(CGAffineTransform(translationX: 0, y: 15))
return label
}()
private lazy var reviewsImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = #imageLiteral(resourceName: "LabelBackground")
return imageView
}()
let stackView = UIStackView()
let buttonA = UIButton()
let buttonB = UIButton()
let buttonC = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
layout()
popupView.addGestureRecognizer(panRecognizer)
}
override var prefersStatusBarHidden: Bool {
return true
}
//Layout
private var bottomConstraint = NSLayoutConstraint()
private func layout() {
contentImageView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(contentImageView)
contentImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
contentImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
contentImageView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
contentImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
overlayView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(overlayView)
overlayView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
overlayView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
overlayView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
overlayView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
popupView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(popupView)
popupView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
popupView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
bottomConstraint = popupView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: popupOffset)
bottomConstraint.isActive = true
popupView.heightAnchor.constraint(equalToConstant: 500).isActive = true
closedTitleLabel.translatesAutoresizingMaskIntoConstraints = false
popupView.addSubview(closedTitleLabel)
closedTitleLabel.leadingAnchor.constraint(equalTo: popupView.leadingAnchor).isActive = true
closedTitleLabel.trailingAnchor.constraint(equalTo: popupView.trailingAnchor).isActive = true
closedTitleLabel.topAnchor.constraint(equalTo: popupView.topAnchor, constant: 20).isActive = true
openTitleLabel.translatesAutoresizingMaskIntoConstraints = false
popupView.addSubview(openTitleLabel)
openTitleLabel.leadingAnchor.constraint(equalTo: popupView.leadingAnchor).isActive = true
openTitleLabel.trailingAnchor.constraint(equalTo: popupView.trailingAnchor).isActive = true
openTitleLabel.topAnchor.constraint(equalTo: popupView.topAnchor, constant: 20).isActive = true
reviewsImageView.translatesAutoresizingMaskIntoConstraints = false
popupView.addSubview(reviewsImageView)
reviewsImageView.leadingAnchor.constraint(equalTo: popupView.leadingAnchor).isActive = true
reviewsImageView.trailingAnchor.constraint(equalTo: popupView.trailingAnchor).isActive = true
reviewsImageView.bottomAnchor.constraint(equalTo: popupView.bottomAnchor).isActive = true
reviewsImageView.heightAnchor.constraint(equalToConstant: 428).isActive = true
buttonA.backgroundColor = UIColor.clear
let heightConstraintA = buttonA.heightAnchor.constraint(equalToConstant: 135)
heightConstraintA.isActive = true
heightConstraintA.priority = UILayoutPriority(rawValue: 999)
buttonA.translatesAutoresizingMaskIntoConstraints = false
buttonA.setTitle("A", for: .normal)
buttonA.setTitleColor(UIColor.darkGray, for: .normal)
buttonA.backgroundColor = UIColor.clear
buttonA.addTarget(self, action: #selector(buttonATapped(sender:)), for: .touchDown)
//self.popupView.addSubview(buttonA)
buttonB.backgroundColor = UIColor.clear
let heightConstraintB = buttonB.heightAnchor.constraint(equalToConstant: 135)
heightConstraintB.isActive = true
heightConstraintB.priority = UILayoutPriority(rawValue: 999)
buttonB.translatesAutoresizingMaskIntoConstraints = false
buttonB.setTitle("B", for: .normal)
buttonB.setTitleColor(UIColor.darkGray, for: .normal)
buttonB.backgroundColor = UIColor.clear
//self.popupView.addSubview(buttonB)
buttonC.backgroundColor = UIColor.clear
let heightConstraintC = buttonC.heightAnchor.constraint(equalToConstant: 135)
heightConstraintC.isActive = true
heightConstraintC.priority = UILayoutPriority(rawValue: 999)
buttonC.translatesAutoresizingMaskIntoConstraints = false
buttonC.setTitle("C", for: .normal)
buttonC.setTitleColor(UIColor.darkGray, for: .normal)
buttonC.backgroundColor = UIColor.clear
//self.popupView.addSubview(buttonC)
popupView.addSubview(stackView)
stackView.backgroundColor = UIColor.clear
stackView.addArrangedSubview(buttonA)
stackView.addArrangedSubview(buttonB)
stackView.addArrangedSubview(buttonC)
stackView.translatesAutoresizingMaskIntoConstraints = false
popupView.addSubview(stackView)
stackView.leadingAnchor.constraint(equalTo: popupView.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: popupView.trailingAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: popupView.bottomAnchor).isActive = true
stackView.heightAnchor.constraint(equalToConstant: 428).isActive = true
stackView.axis = .vertical
stackView.distribution = .fill
stackView.translatesAutoresizingMaskIntoConstraints = false
}
#objc func buttonATapped(sender: UIButton) {
print ("test")
}
private func animateTransitionIfNeeded(to state: State, duration: TimeInterval) {
//Animation code
}
#objc private func popupViewPanned(recognizer: UIPanGestureRecognizer) {
//Animation code
}
}
***For anyone else having the same issue, here is how I solved it, thanks to #OverD:
I removed the reviewsImageView completely (because that was only for color in my case, and I can easily add the color to the UIButton instead) Then instead of adding the buttons to the popupView, I added them to the stack view, and the stack view to the popupView. Lastly, my syntax for addTarget was not correct, and for some reason changing it to touchDown instead of touchUpInside made it work.
I noticed from the provided code that you are adding the same buttons multiple times, once as a subview of the popView and once in the stackView. Also you are not assigning any targets for the buttons.
I hope this helps

Why isn't UIButton returning correct constraints?

In my code below:
I have 5 buttons added into a vertical scrollView. Each button is constrained to the scrollViews's top + 20 ,leading, trailing edges and its height. I have created a b1HeightConstraint variable. It's there to hold the heightConstraint of the b1 button.
In a button click, I'm trying to remove this constraint. Yet I'm facing an odd issue:
When I log the constraints I only see 2 constraints, even though I've added 4 constraints to it. My the view debug hierarchy is like below:
import UIKit
import Foundation
class ViewController: UIViewController {
var filterView: UIView!
var scrollView: UIScrollView!
var containerView: UIView!
override func loadView() {
filterView = UIView()
view = filterView
view.backgroundColor = #colorLiteral(red: 0.909803926944733, green: 0.47843137383461, blue: 0.643137276172638, alpha: 1.0)
scrollView = UIScrollView()
scrollView.backgroundColor = #colorLiteral(red: 0.474509805440903, green: 0.839215695858002, blue: 0.976470589637756, alpha: 1.0)
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
scrollView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 1).isActive = true
scrollView.isScrollEnabled = true
containerView = UIView()
containerView.backgroundColor = #colorLiteral(red: 0.176470592617989, green: 0.498039215803146, blue: 0.756862759590149, alpha: 1.0)
scrollView.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
// This is key: connect all four edges of the containerView to
// to the edges of the scrollView
containerView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
containerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
containerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
// Making containerView and scrollView the same height means the
// content will not scroll vertically
containerView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
}
let b1 = Buttons(titleText: "one")
let b2 = Buttons(titleText: "two")
let b3 = Buttons(titleText: "three")
let b4 = Buttons(titleText: "four")
let b5 = Buttons(titleText: "five")
var b1HeightConstraint : NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
let buttonArray = [b1, b2, b3, b4, b5]
b1.button.addTarget(self, action: #selector(ViewController.shrink(_:)), for: .touchUpInside)
var startPoint = containerView.topAnchor
for btn in buttonArray {
let theBtn = btn.button
containerView.addSubview(theBtn)
theBtn.translatesAutoresizingMaskIntoConstraints = false
theBtn.topAnchor.constraint(equalTo: startPoint, constant: 20).isActive = true
theBtn.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
theBtn.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
theBtn.heightAnchor.constraint(equalTo: scrollView.heightAnchor).isActive = true
startPoint = theBtn.bottomAnchor
let btnHeight = theBtn.heightAnchor.constraint(equalTo: scrollView.heightAnchor)
if btn == b1{
b1HeightConstraint = btnHeight
}
}
containerView.bottomAnchor.constraint(equalTo: startPoint, constant: 20).isActive = true
}
#objc func shrink(_ sender: Any){
guard let btn = sender as? UIButton else{
return
}
print("count is: \(btn.constraints.count)")
btn.removeConstraint(b1HeightConstraint!)
containerView.removeConstraint(b1HeightConstraint!)
print("count is: \(btn.constraints.count)")
containerView.updateConstraintsIfNeeded()
containerView.updateConstraints()
scrollView.updateConstraintsIfNeeded()
scrollView.updateConstraints()
}
}
class Buttons : NSObject {
let button = UIButton()
init(titleText: String) {
button.backgroundColor = #colorLiteral(red: 0.976470589637756, green: 0.850980401039124, blue: 0.549019634723663, alpha: 1.0)
button.setTitle(titleText, for: .normal)
}
}
The code is ready to just be dumpped in the ViewController class. Works out of the box. My code is a spinoff of the code written here
Here are several comments about your code:
You never added any constraints to any views, so you shouldn't be removing them. iOS (CocoaTouch) added those constraints to those views, so please don't touch them. (In other words: don't call removeConstraint when you didn't call addConstraint). Your control over constraints is activating and deactivating them. Leave the adding and removing to iOS.
When you activate a constraint, a constraint is added (by iOS) to the most common ancestor of the two items mentioned in the constraint. So if the two views are siblings, it will be added to the parent. If the two views are parent and child, the constraint will be added to the parent. If the two views are grandparent and grandchild, it will be added to the grandparent. If the two views are first cousins, the constraint will be added to their common grandparent.
These lines of code:
let btnHeight = theBtn.heightAnchor.constraint(equalTo: scrollView.heightAnchor)
if btn == b1{
b1HeightConstraint = btnHeight
}
are creating a new constraint and assigning it to b1HeightConstraint, but you never activated this constraint, so it hasn't have been added to any view at all. So trying to remove it was never going to work, because that constraint exists only in your b1HeightConstraint property. Since it was never activated, it isn't actually constraining anything.
If you want to shrink a button, you need to do one of these: a) modify the constant property of its height constraint OR b) set its height constraint's isActive property to false and then give it a new height constraint OR c) modify the priorities of the active constraints to have Auto Layout choose to use different constraints.
In your view debug hierarchy, all the constraints shown are active constraints (meaning they are available to be used by Auto Layout). The grayed out ones are the ones Auto Layout chose not to use because a higher priority constraint had precedence over it. This causes no conflict. The self.height = 34 (content size) constraint is added by the system to account for content compression and content hugging. UIButtons resist compression with priority 750 and resist expansion with priority 250. The self.height = 34 (content size) constraint is grayed out because content hugging has a priority of 250 and another higher priority constraint was used instead (the constraint which sets the button's height equal to the scrollView's height has priority 1000).
Updated Code:
Here is your modified code. I changed two things:
I made sure b1HeightConstraint was an activated constraint.
I changed the shrink method to deactivate the old height constraint and then create and activate a new one.
Updated code
import UIKit
import Foundation
class ViewController: UIViewController {
var filterView: UIView!
var scrollView: UIScrollView!
var containerView: UIView!
override func loadView() {
filterView = UIView()
view = filterView
view.backgroundColor = #colorLiteral(red: 0.909803926944733, green: 0.47843137383461, blue: 0.643137276172638, alpha: 1.0)
scrollView = UIScrollView()
scrollView.backgroundColor = #colorLiteral(red: 0.474509805440903, green: 0.839215695858002, blue: 0.976470589637756, alpha: 1.0)
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
scrollView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 1).isActive = true
scrollView.isScrollEnabled = true
containerView = UIView()
containerView.backgroundColor = #colorLiteral(red: 0.176470592617989, green: 0.498039215803146, blue: 0.756862759590149, alpha: 1.0)
scrollView.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
// This is key: connect all four edges of the containerView to
// to the edges of the scrollView
containerView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
containerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
containerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
// Making containerView and scrollView the same height means the
// content will not scroll vertically
containerView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
}
let b1 = Buttons(titleText: "one")
let b2 = Buttons(titleText: "two")
let b3 = Buttons(titleText: "three")
let b4 = Buttons(titleText: "four")
let b5 = Buttons(titleText: "five")
var b1HeightConstraint : NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
let buttonArray = [b1, b2, b3, b4, b5]
b1.button.addTarget(self, action: #selector(ViewController.shrink(_:)), for: .touchUpInside)
var startPoint = containerView.topAnchor
for btn in buttonArray {
let theBtn = btn.button
containerView.addSubview(theBtn)
theBtn.translatesAutoresizingMaskIntoConstraints = false
theBtn.topAnchor.constraint(equalTo: startPoint, constant: 20).isActive = true
theBtn.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
theBtn.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
//theBtn.heightAnchor.constraint(equalTo: scrollView.heightAnchor).isActive = true
startPoint = theBtn.bottomAnchor
let btnHeight = theBtn.heightAnchor.constraint(equalTo: scrollView.heightAnchor)
btnHeight.isActive = true
if btn == b1{
b1HeightConstraint = btnHeight
}
}
containerView.bottomAnchor.constraint(equalTo: startPoint, constant: 20).isActive = true
}
#objc func shrink(_ sender: UIButton) {
b1HeightConstraint?.isActive = false
b1HeightConstraint = sender.heightAnchor.constraint(equalToConstant: 20)
b1HeightConstraint?.isActive = true
}
}
class Buttons : NSObject {
let button = UIButton()
init(titleText: String) {
button.backgroundColor = #colorLiteral(red: 0.976470589637756, green: 0.850980401039124, blue: 0.549019634723663, alpha: 1.0)
button.setTitle(titleText, for: .normal)
}
}
Options for shrinking the button's height
Setting the constant property of the height constraint
// shrink button's height by 200 points
b1HeightConstraint?.constant -= 200
Deactivate the old constraint and create and activate a new one
// make button height 20 points
b1HeightConstraint?.isActive = false
b1HeightConstraint = sender.heightAnchor.constraint(equalToConstant: 20)
b1HeightConstraint?.isActive = true
Change the priority of the height constraint
// Set b1HeightConstraint's priority to less than 250, and the
// *content hugging* with priority 250 will take over and resize
// the button to its intrinsic height
b1HeightConstraint?.priority = UILayoutPriority(rawValue: 100)
Note: For this to work, you have to give the height constraint an initial priority less than 1000 (999 works nicely) because Auto Layout will not let you change the priority of an active constraint if it is required (priority 1000).
OR
// Just deactivate the buttonHeight constraint and the *content
// hugging* will take over and it will set the button's height
// to its intrinsic height
b1HeightConstraint?.isActive = false
This is because a constraint between a view and its superView is added to the superView , you only see height/width constraint if they are static added to the UIButton , look to this diagram from Vandad IOS Book
see this Demo

Resources