Dynamically add buttons in swift - ios

I am writing an iOS app. Server gives me buttons array to draw on view. So, the number of buttons can vary according to server response,
example:
let buttonArray=["Button 1","Button 2","Button 3"]
or
let buttonArray=["Button 1","Button 2","Button 3"," Button 4", "Button 5"]
I have to stack these buttons vertically.
I created a stackview, add constraints to it, and then add buttons to this stackview as arrangedsubviews.
Buttons should have gap of 5 points between them:
Using stackview:
func addButtonsUsingStackView()
{
//create stackview:
let stackView=UIStackView()
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.alignment = .fill
stackView.spacing = 5
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
//stackview constraints:
let viewsDictionary = ["stackView":stackView]
let stackView_H = NSLayoutConstraint.constraints(
withVisualFormat: "H:|-20-[stackView]-20-|",
options: NSLayoutFormatOptions(rawValue: 0),
metrics: nil,
views: viewsDictionary)
let stackView_V = NSLayoutConstraint.constraints(
withVisualFormat: "V:|-30-[stackView]-30-|",
options: NSLayoutFormatOptions(rawValue:0),
metrics: nil,
views: viewsDictionary)
view.addConstraints(stackView_H)
view.addConstraints(stackView_V)
//adding buttons to stackview:
//let buttonArray=["Button 1","Button 2","Button 3"]
let buttonArray=["Button 1","Button 2","Button 3"," Button 4"]
for buttonName in buttonArray{
let button=UIButton()
button.setTitle(buttonName, for: .normal)
button.setTitleColor(UIColor.white, for: .normal)
button.backgroundColor=UIColor.blue
button.translatesAutoresizingMaskIntoConstraints=false
stackView.addArrangedSubview(button)
}
}
Without stackview:
var buttonArray=["Button 1","Button 2","Button 3"," Button 4"," Button 5"," Button 6"," Button 7"]
func addButtonsLoop()
{
for _view in view.subviews{
_view.removeFromSuperview()
}
var i=0
var buttonY = 20
let buttonGap=5
for btn in buttonArray {
let buttonHeight=Int(Int(view.frame.height) - 40 - (buttonArray.count * buttonGap))/buttonArray.count
print(buttonHeight)
let buttonWidth=Int(view.frame.width - 40)
let button = UIButton()
button.backgroundColor = UIColor.orange
button.setTitle(btn, for: .normal)
button.titleLabel?.textColor = UIColor.white
button.frame = CGRect(x: 20, y: buttonY, width:buttonWidth , height:buttonHeight)
button.contentMode = UIViewContentMode.scaleToFill
buttonY += buttonHeight + buttonGap
button.tag = i
button.addTarget(self, action: #selector(self.buttonTapped(_:)), for: UIControlEvents.touchUpInside)
view.addSubview(button)
i+=1
}
}
func buttonTapped( _ button : UIButton)
{
buttonArray.remove(at: button.tag)
addButtonsLoop()
}
My question is that instead of above code, how to apply NSLayoutConstraints or LayoutAnchors to solve this?

Instead of stack view you can use a tableview with the tableview cell containing a button and the number of rows can be the buttonarray count. In cellForRowAtIndexPath delegate method set the button title from button array.

you can use scroll view to add buttons.
var btnY = 5
let btnHeight = 40
func addButtonsUsingStackView()
{
for view in self.view.subviews{
view.removeFromSuperview()
}
for i in 0..< buttonArray.count {
let btnFloor = UIButton()
btnFloor.backgroundColor = Orange
btnFloor.titleLabel?.textColor = UIColor.white
btnFloor.frame = CGRect(x: 10, y: btnY, width: Int(scrView.frame.width - 20), height: btnHeight)
btnFloor.contentMode = UIViewContentMode.scaleToFill
btnY += btnHeight + 5
btnFloor.tag = buttonArray.index(of: i)
btnFloor.addTarget(self, action: #selector(self.btnTappedFloor(_:)), for: UIControlEvents.touchUpInside)
self.view.addSubview(btnFloor)
}
return cell
}
func btnTappedFloor( _ button : UIButton)
{
buttonArray.remove(at: button.tag)
addButtonsUsingStackView()
}

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.animate grows the rectangle from bottom-left to top-right instead of straight up vertically

I am creating a pulldown menu using a stackview in conjunction with a textfield. When the textfield is tapped, I animate the contents of the stackview by "growing" the stackview upwards, but it is doing it from the bottom-left to the top-right corner. How can I make it "grow" straight up vertically upwards? I suspect that it may be due to the width of the buttons not being equal to the width of the stackview, but I have already set the stackview's alignment property to .fill as well as set a constraint to make the stackview's width anchor equal to the textfield's width anchor. So, I am not sure what I need to fix. Here's the code followed by a clip of how it looks right now.
import UIKit
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var textfield: UITextField!
#IBOutlet weak var label: UILabel!
var popup: UIStackView = {
let selectionList = ["Apple",
"Grapefriut",
"Peach",
"Orange",
"Pear ",
]
let pop = UIStackView()
pop.axis = .vertical
pop.spacing = 0
pop.distribution = .fillEqually
pop.alignment = .fill
pop.layer.borderWidth = 1
pop.layer.borderColor = UIColor.darkGray.cgColor
pop.layer.cornerRadius = 6
pop.clipsToBounds = true
for selection in selectionList {
let button = UIButton()
button.setTitle(selection, for: .normal)
button.backgroundColor = #colorLiteral(red: 0.8202667832, green: 0.9491913915, blue: 1, alpha: 1)
button.setTitleColor(.darkGray, for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 14)
button.addTarget(self, action: #selector(selectionTapped), for: .touchUpInside)
button.isHidden = true
pop.addArrangedSubview(button)
}
return pop
}()
override func viewDidLoad() {
super.viewDidLoad()
textfield.delegate = self
let imageView = UIImageView(image: UIImage(named: "downarrow20x20"))
imageView.contentMode = .center
imageView.frame = CGRect(x: 0, y: 0, width: imageView.image!.size.width + 20.0, height: imageView.image!.size.height)
textfield.rightView = imageView
textfield.rightViewMode = .always
textfield.inputView = UIView() // disable keyboard from popping up
view.addSubview(popup)
popup.translatesAutoresizingMaskIntoConstraints = false
popup.bottomAnchor.constraint(equalTo: textfield.bottomAnchor).isActive = true
popup.widthAnchor.constraint(equalTo: textfield.widthAnchor).isActive = true
popup.centerXAnchor.constraint(equalTo: textfield.centerXAnchor).isActive = true
}
func textFieldDidBeginEditing(_ textField: UITextField) {
showHidePopupContents(hide: false)
}
private func showHidePopupContents(hide: Bool) {
UIView.animate(withDuration: 0.3) {
self.popup.subviews.forEach { btn in
btn.isHidden = hide
}
}
}
#objc func selectionTapped(_ button: UIButton) {
if let buttonStr = button.title(for: .normal) {
let str = buttonStr.trimmingCharacters(in: .whitespacesAndNewlines)
label.text = "\(str) selected"
textfield.text = str
}
// get focus out of textfield
textfield.isEnabled = false
textfield.isEnabled = true
showHidePopupContents(hide: true)
}
}

Custom Button Tap not working in UIView In Table Footer View

I have the most peculiar scenario that I can't seem to fix. I have a custom button that I add as a subview to a UIView. I then add the UIView to the tableFooterView of a table view and I'm not able to have the button tap be detected. Here is the code:
public func configureMyButton() {
let button = CustomButton("My title")
button.addTarget(self, action: #selector(self.buttonAction), for: .touchUpInside)
button.isUserInteractionEnabled = true
let buttonContainer = UIView()
buttonContainer.addSubview(button)
buttonContainer.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-16-[button]", options: [], metrics: [:], views: ["button":button]))
buttonContainer.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-8-[button]-8-|", options: [], metrics: [:], views: ["button":button]))
self.tableView.tableFooterView = buttonContainer
}
#objc func buttonAction(sender: UIButton!) {
print("Button tapped")
}
Now if I change this:
self.view.addSubview(buttonContainer)
The button tap works. This leads me to believe that theres something about tableFooterView that stops the tap from working but I'm not entirely sure what it could be. Any ideas?
The reason why the button wasn't responding to taps was because the buttonContainers frame was completely wrong. So despite everything looking find on screen, the frame was practically none existent and hence the button wasn't responding
public func configureMyButton() {
let button = CustomButton("My title")
button.addTarget(self, action: #selector(self.buttonAction), for: .touchUpInside)
button.isUserInteractionEnabled = true
let buttonContainer = UIView()
buttonContainer.addSubview(button)
buttonContainer.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-16-[button]", options: [], metrics: [:], views: ["button":button]))
buttonContainer.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-8-[button]-8-|", options: [], metrics: [:], views: ["button":button]))
buttonContainer.layoutIfNeeded()
buttonContainer.frame = CGRect(x: 0, y: 0, width: self.tableView.frame.size.height, height: button.frame.size.height + 16)
self.tableView.tableFooterView = buttonContainer
}
I'm not overly happy with the solution. I'm of the opinion I shouldn't have to have fiddled with the buttonContainer frame. Autolayout should have deduced it's frame to at the very least be the size of its subviews.
As you noted, the button could not be tapped because it was displayed outside the button container frame.
UITableView handles the layout for its header and footer views, so using auto-layout with them takes an additional step.
Don't add your footer view in viewDidLoad(). Instead, override viewDidLayoutSubviews() like this:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// manipulating the tableFooterView will trigger viewDidLayoutSubviews()
// so only call this if we haven't added the footer view yet
if tableView.tableFooterView == nil {
configureMyButton()
tableView.layoutTableFooterView()
}
}
Change your configureMyButton() func as shown here:
public func configureMyButton() {
// I don't have your CustomButton() func...
//let button = CustomButton("My title")
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("My Title", for: .normal)
button.backgroundColor = .blue
button.addTarget(self, action: #selector(self.buttonAction), for: .touchUpInside)
button.isUserInteractionEnabled = true
let buttonContainer = UIView()
// set background to red so we can see it - remove after testing
buttonContainer.backgroundColor = .red
buttonContainer.addSubview(button)
buttonContainer.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-16-[button]|", options: [], metrics: [:], views: ["button":button]))
buttonContainer.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-8-[button]-8-|", options: [], metrics: [:], views: ["button":button]))
self.tableView.tableFooterView = buttonContainer
}
And then add this extension:
extension UITableView {
func layoutTableHeaderView() {
guard let tempView = self.tableHeaderView else { return }
tempView.translatesAutoresizingMaskIntoConstraints = false
let width = tempView.bounds.size.width;
let temporaryWidthConstraints = NSLayoutConstraint.constraints(withVisualFormat: "[tempView(width)]", options: NSLayoutConstraint.FormatOptions(rawValue: UInt(0)), metrics: ["width": width], views: ["tempView": tempView])
tempView.addConstraints(temporaryWidthConstraints)
tempView.setNeedsLayout()
tempView.layoutIfNeeded()
let tempSize = tempView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
let height = tempSize.height
var frame = tempView.frame
frame.size.height = height
tempView.frame = frame
self.tableHeaderView = tempView
tempView.removeConstraints(temporaryWidthConstraints)
tempView.translatesAutoresizingMaskIntoConstraints = true
}
func layoutTableFooterView() {
guard let tempView = self.tableFooterView else { return }
tempView.translatesAutoresizingMaskIntoConstraints = false
let width = tempView.bounds.size.width;
let temporaryWidthConstraints = NSLayoutConstraint.constraints(withVisualFormat: "[tempView(width)]", options: NSLayoutConstraint.FormatOptions(rawValue: UInt(0)), metrics: ["width": width], views: ["tempView": tempView])
tempView.addConstraints(temporaryWidthConstraints)
tempView.setNeedsLayout()
tempView.layoutIfNeeded()
let tempSize = tempView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
let height = tempSize.height
var frame = tempView.frame
frame.size.height = height
tempView.frame = frame
self.tableFooterView = tempView
tempView.removeConstraints(temporaryWidthConstraints)
tempView.translatesAutoresizingMaskIntoConstraints = true
}
}
Now your footer view will size correctly based on your auto-layout constraints -- so if you add elements to the footer view you won't have to explicitly change your height value.

Swift: Buttons not working inside UIScrollView

I have a ScrollView. You can scroll but the buttons don't allow you to press them(when you press them they don't do anything). Can you help fix this? Here is my current code:
PS: I'm checking this regularly so if you have a question I'll answer.
class ViewController: UIViewController {
#IBOutlet weak var categoryScrollView: UIScrollView!
var categoryArr = ["Button1","Button2","Button3","Button4","Button5", "Button 6", "Button 7", "Button 8", "Button 9", "Button 10", "Button 11", "Button 12"]
var buttonColors = [UIColor.greenColor(), UIColor.blueColor(), UIColor.blackColor(), UIColor.cyanColor(), UIColor.magentaColor(), UIColor.greenColor(), UIColor.blueColor(), UIColor.blackColor(), UIColor.cyanColor(), UIColor.magentaColor(), UIColor.blackColor(), UIColor.brownColor()]
var buttonImages = [UIImage(named: "One"), UIImage(named: "Two"), UIImage(named: "Three"), UIImage(named: "PlayButtonImage"), UIImage(named: "Triangle"), UIImage(named: "PlayButtonImage"), UIImage(named: "Triangle"), UIImage(named: "PlayButtonImage"), UIImage(named: "Triangle"), UIImage(named: "PlayButtonImage"), UIImage(named: "Triangle"), UIImage(named: "PlayButtonImage")]
let kPadding:CGFloat = 20
override func viewDidLoad() {
super.viewDidLoad()
let buttonSize = CGSizeMake(categoryScrollView.bounds.size.width/2, categoryScrollView.bounds.size.height/2)//hal
let scrollingView = ImageButtonsView(buttonSize, buttonCount: 12)
categoryScrollView.contentSize = scrollingView.frame.size
categoryScrollView.addSubview(scrollingView)
categoryScrollView.showsVerticalScrollIndicator = false
categoryScrollView.delegate = self
categoryScrollView.pagingEnabled = true
categoryScrollView.indicatorStyle = .Default
categoryScrollView.contentOffset = CGPointMake(0, 0)
categoryScrollView.delaysContentTouches = false
categoryScrollView.userInteractionEnabled = true;
categoryScrollView.exclusiveTouch = true;
categoryScrollView.canCancelContentTouches = true;
//categoryScrollView.clipsToBounds = true
}
func ImageButtonsView(buttonSize:CGSize, buttonCount:Int) -> UIView {
let buttonView = UIView()
buttonView.frame.origin = CGPointMake(50,142)
let padding = CGSizeMake(kPadding, kPadding)
buttonView.frame.size.width = (buttonSize.width + padding.width) * CGFloat(buttonCount)
var buttonPosition = CGPointMake(0, padding.height)
let buttonIncrement = buttonSize.width + padding.width
for i in 0...(buttonCount - 1) {
let button = UIButton(type: .Custom)
buttonView.userInteractionEnabled = true
button.frame.size = buttonSize
button.frame.origin = buttonPosition
buttonPosition.x = buttonPosition.x + buttonIncrement
button.setTitle(categoryArr[i], forState: UIControlState.Normal)
let buttonImagesOne = buttonImages[i]
button.setImage(buttonImagesOne, forState: .Normal)
button.layer.cornerRadius = 30
button.addTarget(self, action: "pressed:", forControlEvents: UIControlEvents.TouchUpInside)
buttonView.addSubview(button)
}
//buttonView.backgroundColor = UIColor.redColor()
categoryScrollView.bringSubviewToFront(buttonView)
return buttonView
}
}
extension ViewController:UIScrollViewDelegate{
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let index = round(scrollView.contentOffset.x / scrollView.frame.size.width)
print(index)
}
func pressed(/*sender: UIButton!*/) {
let vc = UIViewController(nibName: "GridViewController", bundle: nil)
self.presentViewController(vc, animated: true, completion:nil)
//viewController.hidden = true
}
}
Originally here, I thought the line
button.addTarget(self, action: "pressed:", forControlEvents: UIControlEvents.TouchUpInside)
should have said "pressed" without a ":". But that turned out to be a misunderstanding on my part, sorry. It should be "pressed:".
Instead, this may be related to Protocol extension and addTarget is crashing with NSInvalidArgumentException which is also very confusing.
It turned out my scrollview's layout was set to "Autoresizing Mask" instead of "Inferred (Constraints)". Although my layout looked good, my buttons were actually outside the bounds of the scrollview and therefore couldn't be clicked!
Have you tried adding a UITapGestureRecognizer to your button? You could try something like this:
let myTap = UITapGestureRecognizer(target: self, action: Selector("handleButtonTap:"))
myTap.delegate = self
//**Make your class inherit from UIGestureRecognizerDelegate otherwise an error will be thrown**
myButton.addGestureRecognizer(myTap)
Then write a function (the name of the function should be what you put in the action field above. So in this case the function would be called handleButtonTap. Here is the code:
func handleButtonTap() {
// Handle what happens when the button is tapped.
}
I hope I was able to help :).
This is my approach (Swift 2 / xCode 7.2.1):
I have 2 View Controllers:
“VC1” with the scrollview
Here I create the different views (instances of “VC2”) and add them to the scrollview:
scrollView.addSubview(pageStep.view)
addChildViewController(pageStep)
which I call page1, page2, page3 …
Then you can access the "page1.btn1" and do something like this
page1.nextBtn.addTarget(self, action: "nextPage:", forControlEvents: .TouchUpInside)
func nextPage(sender: UIButton!) {
let xPosition = (pageFraction + 1) * scrollSize
scrollView.setContentOffset(CGPoint(x: xPosition, y: 0), animated: true)
}
“VC2” With the outlets (each page) (UIImage, Buttons etc.)
I hope it helps !

Swift: How to use buttons in Scroll View

I have created a scroll view with "buttons". But the "buttons" actually don't do anything at the moment. So, I would like to know, how I can make the "buttons" usable. So when I press them they lead me to a different view controller. All the buttons will lead me to the same view controller just with different data (depending on what I want that button to represent) inside the view controller. Here's my code so far:
#IBOutlet weak var categoryScrollView: UIScrollView!
var categoryArr = ["Button1","Button2","Button3","Button4","Button5", "Button 6", "Button 7", "Button 8", "Button 9", "Button 10", "Button 11", "Button 12"]
var buttonColors = [UIColor.greenColor(), UIColor.blueColor(), UIColor.blackColor(), UIColor.cyanColor(), UIColor.magentaColor(), UIColor.greenColor(), UIColor.blueColor(), UIColor.blackColor(), UIColor.cyanColor(), UIColor.magentaColor(), UIColor.blackColor(), UIColor.brownColor()]
let kPadding:CGFloat = 20
override func viewDidLoad() {
super.viewDidLoad()
let buttonSize = CGSizeMake(categoryScrollView.bounds.size.width/2, categoryScrollView.bounds.size.height/2)//hal
let scrollingView = colorButtonsView(buttonSize, buttonCount: 12)
categoryScrollView.contentSize = scrollingView.frame.size
categoryScrollView.addSubview(scrollingView)
categoryScrollView.showsVerticalScrollIndicator = false
categoryScrollView.delegate = self
categoryScrollView.pagingEnabled = true
categoryScrollView.indicatorStyle = .Default
categoryScrollView.contentOffset = CGPointMake(0, 0)
}
func colorButtonsView(buttonSize:CGSize, buttonCount:Int) -> UIView {
let buttonView = UIView()
buttonView.frame.origin = CGPointMake(50,300)
let padding = CGSizeMake(kPadding, kPadding)
buttonView.frame.size.width = (buttonSize.width + padding.width) * CGFloat(buttonCount)
var buttonPosition = CGPointMake(0, padding.height)
let buttonIncrement = buttonSize.width + padding.width
for i in 0...(buttonCount - 1) {
let button = UIButton(type: .Custom)
button.frame.size = buttonSize
button.frame.origin = buttonPosition
buttonPosition.x = buttonPosition.x + buttonIncrement
button.setTitle(categoryArr[i], forState: UIControlState.Normal)
button.backgroundColor = buttonColors[i]
buttonView.addSubview(button)
}
buttonView.backgroundColor = UIColor.redColor()
return buttonView
}
}
extension ViewController:UIScrollViewDelegate{
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let index = round(scrollView.contentOffset.x / scrollView.frame.size.width)
print(index)
}
}
When you create your button you have to specify a target in order to have an action with it (add this in your for loop) :
button.addTarget(self, action: "myaction:", forControlEvents: .TouchUpInside)
then create the corresponding method
func myaction(sender: UIButton!) {
// navigation code here.
}
Please note that you don't have to create several method in order to have different actions for each button, all you have to do is something like this :
button.tag = i
and then in your action method :
switch sender.tag {
case 0 :
//do some stuff
case 1 :
//do some stuff
}
You have to add targets to button to fire some action methods. Like in your loop when you create UIButton, add
button.addTarget(self, action: "pressed:", forControlEvents: .TouchUpInside)
and implement one method like:
func pressed(sender: UIButton!) {
// navigation code here.
}

Resources