Arranging buttons programmatically with constraints - ios

I have an array of buttons that I am iterating through and adding the buttons onto the view. Each button should be adjacent to the previous button, so I'm setting the leading constraint to the previous button's trailing. But the buttons end up layered on top of each other with only the top one displayed.
for k in 0 ..< buttons.count {
view.addSubview(buttons[k])
if k > 0 {
buttons[k].leadingAnchor.constraint(equalTo: buttons[k-1].trailingAnchor).isActive = true
}
}
Edit:
I don't know if this is part of the problem, but here's how I'm creating the buttons. I set each to (0,0) because I don't know where they'll end up. I assume the constraint would reposition them as needed (first time use programmatic constraints).
let size = CGRect(x: 0, y: 0, width: buttonWidth, height: buttonHeight)
let button: UIButton = UIButton(frame: size)

Here a simple playground that works with a UIStackView. You can play a bit and accommodate for your goal.
UIStackViews are very flexible components if you want avoid creating constraints manually.
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyViewController: UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let buttons = createButtons()
let stackView = createStackView(with: UILayoutConstraintAxis.vertical)
buttons.forEach { button in
stackView.addArrangedSubview(button)
}
view.addSubview(stackView)
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
self.view = view
}
func createStackView(with layout: UILayoutConstraintAxis) -> UIStackView {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = layout
stackView.distribution = .equalSpacing
stackView.spacing = 0
return stackView
}
func createButtons() -> [UIButton] {
var buttons = [UIButton]()
for x in 0..<5 {
let button = UIButton(type: .custom)
button.backgroundColor = .red
button.translatesAutoresizingMaskIntoConstraints = false
button.widthAnchor.constraint(equalToConstant: 50).isActive = true
button.heightAnchor.constraint(equalToConstant: 100).isActive = true
button.setTitle("Title \(x)", for: .normal)
buttons.append(button)
}
return buttons
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

The key problem is you should use isActive to active constraint.
The following is example
var buttons: [UIButton] = []
for index in 0...5 {
let button = UIButton(frame: .zero)
button.setTitleColor(.black, for: .normal)
button.setTitle("button \(index)", for: .normal)
button.layer.borderColor = UIColor.gray.cgColor
button.layer.borderWidth = 1.0
button.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(button)
buttons.append(button)
}
for index in 0...5 {
let button = buttons[index]
if index == 0 {
button.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 8.0).isActive = true
button.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 20.0).isActive = true
} else {
let preButton = buttons[index - 1]
button.leadingAnchor.constraint(equalTo: preButton.trailingAnchor, constant: 8.0).isActive = true
button.topAnchor.constraint(equalTo: preButton.topAnchor, constant: 0.0).isActive = true
}
}

Related

Weird horizontal shrinking animation when hiding UIButton with Configuration in UIStackView

I'm facing this weird animation issues when hiding UIButton in a StackView using the new iOS 15 Configuration. See playground:
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
private weak var contentStackView: UIStackView!
override func viewDidLoad() {
view.frame = CGRect(x: 0, y: 0, width: 300, height: 150)
view.backgroundColor = .white
let contentStackView = UIStackView()
contentStackView.spacing = 8
contentStackView.axis = .vertical
for _ in 1...2 {
contentStackView.addArrangedSubview(makeConfigurationButton())
}
let button = UIButton(type: .system)
button.setTitle("Toggle", for: .normal)
button.addAction(buttonAction, for: .primaryActionTriggered)
view.addSubview(contentStackView)
view.addSubview(button)
contentStackView.translatesAutoresizingMaskIntoConstraints = false
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentStackView.topAnchor.constraint(equalTo: view.topAnchor),
contentStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
contentStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
self.contentStackView = contentStackView
}
private var buttonAction: UIAction {
UIAction { [weak self] _ in
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 1, delay: 0) {
guard let toggleElement = self?.contentStackView.arrangedSubviews[0] else { return }
toggleElement.isHidden.toggle()
toggleElement.alpha = toggleElement.isHidden ? 0 : 1
self?.contentStackView.layoutIfNeeded()
}
}
}
private func makeSystemButton() -> UIButton {
let button = UIButton(type: .system)
button.setTitle("System Button", for: .normal)
return button
}
private func makeConfigurationButton() -> UIButton {
let button = UIButton()
var config = UIButton.Configuration.filled()
config.title = "Configuration Button"
button.configuration = config
return button
}
}
PlaygroundPage.current.liveView = MyViewController()
Which results in this animation:
But I want the animation to look like this, where the button only shrinks vertically:
Which you can replicate in the playground by just swapping contentStackView.addArrangedSubview(makeConfigurationButton()) for contentStackView.addArrangedSubview(makeSystemButton()).
I guess this has something to do with the stack view alignment, setting it to center gives me the desired animation, but then the buttons don't fill the stack view width anymore and setting the width through AutoLayout results in the same animation again... Also, having just one system button in the stack view results in the same weird animation, but why does it behave differently for two system buttons? What would be a good solution for this problem?
As you've seen, the built-in show/hide animation with UIStackView can be quirky (lots of other quirks when you really get into it).
It appears that, when using a button with UIButton.Configuration, the button's width changes from the width assigned by the stack view to its intrinsic width as the animation occurs.
We can get around that by giving the button an explicit height constraint -- but, what if we want to use the intrinsic height (which may not be known in advance)?
Instead of setting the constraint, set the button's Content Compression Resistance Priority::
button.configuration = config
// add this line
button.setContentCompressionResistancePriority(.required, for: .vertical)
return button
And we no longer get the horizontal sizing:
As you will notice, though, the button doesn't "squeeze" vertically... it gets "pushed up" outside the stack view's bounds.
We can avoid that by setting .clipsToBounds = true on the stack view:
If this effect is satisfactory, we're all set.
However, as we can see, the button is still not getting "squeezed." If that is the visual effect we want, we can use a custom "self-stylized" button instead of a Configuration button:
Of course, there is very little visual difference - and looking closely the button's text is not squeezing. If we really, really, really want that to happen, we need to animate a transform instead of using the stack view's default animation.
And... if we are taking advantage of some of the other conveniences with Configurations, using a self-stylized UIButton might not be an option.
If you want to play with the differences, here's some sample code:
class ViewController : UIViewController {
var btnStacks: [UIStackView] = []
override func viewDidLoad() {
view.backgroundColor = .systemYellow
let outerStack = UIStackView()
outerStack.axis = .vertical
outerStack.spacing = 12
for i in 1...3 {
let cv = UIView()
cv.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
let label = UILabel()
label.backgroundColor = .yellow
label.font = .systemFont(ofSize: 15, weight: .light)
let st = UIStackView()
st.axis = .vertical
st.spacing = 8
if i == 1 {
label.text = "Original Configuration Buttons"
for _ in 1...2 {
st.addArrangedSubview(makeOrigConfigurationButton())
}
}
if i == 2 {
label.text = "Resist Compression Configuration Buttons"
for _ in 1...2 {
st.addArrangedSubview(makeConfigurationButton())
}
}
if i == 3 {
label.text = "Custom Buttons"
for _ in 1...2 {
st.addArrangedSubview(makeCustomButton())
}
}
st.translatesAutoresizingMaskIntoConstraints = false
cv.addSubview(st)
NSLayoutConstraint.activate([
label.heightAnchor.constraint(equalToConstant: 28.0),
st.topAnchor.constraint(equalTo: cv.topAnchor),
st.leadingAnchor.constraint(equalTo: cv.leadingAnchor),
st.trailingAnchor.constraint(equalTo: cv.trailingAnchor),
cv.heightAnchor.constraint(equalToConstant: 100.0),
])
btnStacks.append(st)
outerStack.addArrangedSubview(label)
outerStack.addArrangedSubview(cv)
outerStack.setCustomSpacing(2.0, after: label)
}
// a horizontal stack view to hold a label and UISwitch
let ctbStack = UIStackView()
ctbStack.axis = .horizontal
ctbStack.spacing = 8
let label = UILabel()
label.text = "Clips to Bounds"
let ctbSwitch = UISwitch()
ctbSwitch.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged)
ctbStack.addArrangedSubview(label)
ctbStack.addArrangedSubview(ctbSwitch)
// put the label/switch stack in a view so we can center it
let ctbView = UIView()
ctbStack.translatesAutoresizingMaskIntoConstraints = false
ctbView.addSubview(ctbStack)
// button to toggle isHidden/alpha on the first
// button in each stack view
let button = UIButton(type: .system)
button.setTitle("Toggle", for: .normal)
button.backgroundColor = .white
button.addAction(buttonAction, for: .primaryActionTriggered)
outerStack.addArrangedSubview(ctbView)
outerStack.addArrangedSubview(button)
outerStack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(outerStack)
// respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
outerStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
outerStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
outerStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
ctbStack.topAnchor.constraint(equalTo: ctbView.topAnchor),
ctbStack.bottomAnchor.constraint(equalTo: ctbView.bottomAnchor),
ctbStack.centerXAnchor.constraint(equalTo: ctbView.centerXAnchor),
])
}
#objc func switchChanged(_ sender: UISwitch) {
btnStacks.forEach { v in
v.clipsToBounds = sender.isOn
}
}
private var buttonAction: UIAction {
UIAction { [weak self] _ in
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 1.0, delay: 0) {
guard let self = self else { return }
self.btnStacks.forEach { st in
st.arrangedSubviews[0].isHidden.toggle()
st.arrangedSubviews[0].alpha = st.arrangedSubviews[0].isHidden ? 0 : 1
}
}
}
}
private func makeOrigConfigurationButton() -> UIButton {
let button = UIButton()
var config = UIButton.Configuration.filled()
config.title = "Configuration Button"
button.configuration = config
return button
}
private func makeConfigurationButton() -> UIButton {
let button = UIButton()
var config = UIButton.Configuration.filled()
config.title = "Configuration Button"
button.configuration = config
// add this line
button.setContentCompressionResistancePriority(.required, for: .vertical)
return button
}
private func makeCustomButton() -> UIButton {
let button = UIButton()
button.setTitle("Custom Button", for: .normal)
button.setTitleColor(.white, for: .normal)
button.setTitleColor(.lightGray, for: .highlighted)
button.backgroundColor = .systemBlue
button.layer.cornerRadius = 6
return button
}
}
Looks like this:
Edit
Quick example of another "quirk" when it comes to hiding a stack view's arranged subview (excess code in here, but I stripped down the above example):
class MyViewController : UIViewController {
var btnStacks: [UIStackView] = []
override func viewDidLoad() {
view.backgroundColor = .systemYellow
let outerStack = UIStackView()
outerStack.axis = .vertical
outerStack.spacing = 12
let cv = UIView()
cv.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
let label = UILabel()
label.backgroundColor = .yellow
label.font = .systemFont(ofSize: 15, weight: .light)
let st = UIStackView()
st.axis = .vertical
st.spacing = 8
let colors: [UIColor] = [
.cyan, .green, .yellow, .orange, .white
]
label.text = "Labels"
for j in 0..<colors.count {
let v = UILabel()
v.text = "Label"
v.textAlignment = .center
v.backgroundColor = colors[j]
if j == 2 {
v.text = "Height Constraint = 80.0"
v.heightAnchor.constraint(equalToConstant: 80.0).isActive = true
}
st.addArrangedSubview(v)
}
st.translatesAutoresizingMaskIntoConstraints = false
cv.addSubview(st)
NSLayoutConstraint.activate([
label.heightAnchor.constraint(equalToConstant: 28.0),
st.topAnchor.constraint(equalTo: cv.topAnchor),
st.leadingAnchor.constraint(equalTo: cv.leadingAnchor),
st.trailingAnchor.constraint(equalTo: cv.trailingAnchor),
cv.heightAnchor.constraint(equalToConstant: 300.0),
])
btnStacks.append(st)
outerStack.addArrangedSubview(label)
outerStack.addArrangedSubview(cv)
outerStack.setCustomSpacing(2.0, after: label)
// button to toggle isHidden/alpha on the first
// button in each stack view
let button = UIButton(type: .system)
button.setTitle("Toggle", for: .normal)
button.backgroundColor = .white
button.addAction(buttonAction, for: .primaryActionTriggered)
outerStack.addArrangedSubview(button)
outerStack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(outerStack)
// respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
outerStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
outerStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
outerStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
}
private var buttonAction: UIAction {
UIAction { [weak self] _ in
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 1.0, delay: 0) {
guard let self = self else { return }
self.btnStacks.forEach { st in
st.arrangedSubviews[2].isHidden.toggle()
}
}
}
}
}
When this is run and the "Toggle" button is tapped, it will be painfully obvious what's "not-quite-right."
You should add height constraint to buttons and update this constraint while animating. I edit your code just as below.
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
private weak var contentStackView: UIStackView!
override func viewDidLoad() {
view.frame = CGRect(x: 0, y: 0, width: 300, height: 150)
view.backgroundColor = .white
let contentStackView = UIStackView()
contentStackView.spacing = 8
contentStackView.axis = .vertical
for _ in 1...2 {
contentStackView.addArrangedSubview(makeConfigurationButton())
}
let button = UIButton(type: .system)
button.setTitle("Toggle", for: .normal)
button.addAction(buttonAction, for: .primaryActionTriggered)
view.addSubview(contentStackView)
view.addSubview(button)
contentStackView.translatesAutoresizingMaskIntoConstraints = false
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentStackView.topAnchor.constraint(equalTo: view.topAnchor),
contentStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
contentStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
self.contentStackView = contentStackView
}
private var buttonAction: UIAction {
UIAction { [weak self] _ in
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 1, delay: 0) {
guard let toggleElement = self?.contentStackView.arrangedSubviews[0] else { return }
toggleElement.isHidden.toggle()
toggleElement.alpha = toggleElement.isHidden ? 0 : 1
toggleElement.heightAnchor.constraint(equalToConstant: toggleElement.isHidden ? 0 : 50)
self?.contentStackView.layoutIfNeeded()
}
}
}
private func makeSystemButton() -> UIButton {
let button = UIButton(type: .system)
button.setTitle("System Button", for: .normal)
return button
}
private func makeConfigurationButton() -> UIButton {
let button = UIButton()
var config = UIButton.Configuration.filled()
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.heightAnchor.constraint(equalToConstant: 50)
])
config.title = "Configuration Button"
button.configuration = config
return button
}
}
PlaygroundPage.current.liveView = MyViewController()

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:

Constraints for two UIStackView - one under the other

I working on "Hangman" app. On the top I want image, in middle textfield, and bottom couple UIStackView row for letters. Now I try to add constraints for only two UIStackView one under the other, but one is always behind other ie. not see.
This is image where you can se only second stack view (named: stacklView2), and the first i cant see. Why? What I doing wrong?
And this is code:
import UIKit
class ViewController: UIViewController {
var imageView: UIImageView!
var answerTextfield: UITextField!
override func loadView() {
view = UIView()
view.backgroundColor = .white
imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.layer.borderWidth = 1
view.addSubview(imageView)
answerTextfield = UITextField()
answerTextfield.translatesAutoresizingMaskIntoConstraints = false
answerTextfield.textAlignment = .center
answerTextfield.isUserInteractionEnabled = false
answerTextfield.layer.borderWidth = 1
view.addSubview(answerTextfield)
let button1 = UIButton()
button1.backgroundColor = .red
let button2 = UIButton()
button2.backgroundColor = .blue
let stackView1 = UIStackView(arrangedSubviews: [button1, button2])
stackView1.translatesAutoresizingMaskIntoConstraints = false
stackView1.distribution = .fillEqually
stackView1.axis = .horizontal
view.addSubview(stackView1)
let stackView2 = UIStackView(arrangedSubviews: [button2, button1])
stackView2.translatesAutoresizingMaskIntoConstraints = false
stackView2.distribution = .fillEqually
stackView2.axis = .horizontal
view.addSubview(stackView2)
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 5),
imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
answerTextfield.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 20),
answerTextfield.widthAnchor.constraint(equalTo: view.layoutMarginsGuide.widthAnchor, multiplier: 0.5),
answerTextfield.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stackView1.topAnchor.constraint(equalTo: answerTextfield.bottomAnchor, constant: 20),
stackView1.widthAnchor.constraint(equalTo: view.layoutMarginsGuide.widthAnchor, multiplier: 1.0),
stackView1.centerXAnchor.constraint(equalTo: view.layoutMarginsGuide.centerXAnchor),
//stackView1.bottomAnchor.constraint(equalTo: stackView2.layoutMarginsGuide.topAnchor, constant: -5),
stackView2.topAnchor.constraint(equalTo: stackView1.bottomAnchor, constant: 20),
stackView2.widthAnchor.constraint(equalTo: view.layoutMarginsGuide.widthAnchor, multiplier: 1.0),
stackView2.centerXAnchor.constraint(equalTo: view.layoutMarginsGuide.centerXAnchor),
stackView2.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: -5),
])
}
override func viewDidLoad() {
super.viewDidLoad()
imageView.image = UIImage(named: "Hangman-0")
answerTextfield.text = "T E X T F I E L D"
setupNavigationBar()
}
func setupNavigationBar() {
let hintButton = UIButton(type: .infoLight)
hintButton.addTarget(self, action: #selector(hintTapped), for: .touchUpInside)
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: hintButton)
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(newGameTapped))
}
#objc func hintTapped() {
print("Hint button tapped!")
}
#objc func newGameTapped() {
print("New Game button tapped!")
}
}
You use same objects button1 and button2 in both stacks
let stackView1 = UIStackView(arrangedSubviews: [button1, button2])
let stackView2 = UIStackView(arrangedSubviews: [button2, button1])
so it's added to stackView2 only you need to create 2 other different ones as a single element can be added to only 1 view

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

How to center button in horizonatal stackview?

I have a horizontal stackview with two buttons. Now I want, if one button is hidden other should be in the center.
like,
[x x]
[ x ]
Set the distribution attribute of the UIStackView to Fill Equally either from storyboard or programatically:
stackView.distribution = .fillEqually
Once a button is hidden, the other will be centered.
How about using some hidden UIViews?
Declare the stackView in your class:
var myFirstStack: UIStackView!
var bool = true
bool is a boolean that is going to simulate the condition when you'd want to hide or show the button.
Initialize the stackView in viewDidLoad:
myStack = UIStackView(arrangedSubviews: createButtons("1", "2"))
let v1 = UIView()
v1.isHidden = true
let v2 = UIView()
v2.isHidden = true
myStack.insertArrangedSubview(v1, at: 1)
myStack.addArrangedSubview(v2)
myStack.translatesAutoresizingMaskIntoConstraints = false
myStack.axis = .horizontal
myStack.spacing = 20
myStack.distribution = .fillEqually
view.addSubview(myStack)
It uses this function:
func createButtons(_ named: String...) -> [UIButton] {
var i = true
return named.map { name in
let btn = UIButton()
btn.translatesAutoresizingMaskIntoConstraints = false
btn.setTitle(name, for: .normal)
btn.backgroundColor = i ? .yellow : .red
i.toggle()
btn.setTitleColor(.black, for: .normal)
return btn
}
}
Add the autolayout constraints:
myFirstStack.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20).isActive = true
myFirstStack.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20).isActive = true
myFirstStack.heightAnchor.constraint(equalToConstant: 100).isActive = true
myFirstStack.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
Here is what it looks like so far:
When a third button is hit, the UIViews are hidden or shown:
#IBAction func hide(_ sender: Any) {
myFirstStack.arrangedSubviews[0].isHidden = bool //This is the button to hide
myFirstStack.spacing = bool ? -100 : 20 //Adjust the spacing to your liking
myFirstStack.arrangedSubviews[1].isHidden = !bool //Hide or show the first UIView
myFirstStack.arrangedSubviews[3].isHidden = !bool //Hide or show the second UIView
bool.toggle()
}
And here is the result:

Resources