Custom Button Tap not working in UIView In Table Footer View - ios

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.

Related

Update NSLayoutConstraint animation issue

It's a simple test, all codes as below, why lblA has no animation when width changed to 30? but it has animation when width changed to 300.
import UIKit
class ViewController: UIViewController {
let lblA = UILabel()
var lblAWidthConstraint: NSLayoutConstraint?
var tag = false
// let lblB = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.lblA.backgroundColor = .red
self.view.addSubview(self.lblA)
self.lblA.translatesAutoresizingMaskIntoConstraints = false
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "|[v]"
, options: []
, metrics: nil
, views: ["v" : self.lblA]))
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[v]|"
, options: []
, metrics: nil
, views: ["v" : self.lblA]))
let widthConstraint = self.lblA.widthAnchor.constraint(equalToConstant: 30)
widthConstraint.isActive = true
self.lblAWidthConstraint = widthConstraint
//
let tap = UITapGestureRecognizer(target: self, action: #selector(tapView))
self.view.addGestureRecognizer(tap)
}
#objc func tapView() {
tag = !tag
self.lblAWidthConstraint?.constant = tag ? 300 : 30
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
}
[UPDATE] Finally I found this: Animating Frame of UILabel smoothly

Dynamically add buttons in swift

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()
}

I want to add list of buttons in the same line in Swift 3.0

What I want is to add list of buttons (the number come from the service) to a uiview programmatically, so I think is I have to check if the space between the last button and the end of UIView is enough to add button or I have to go to the next line? Right?
Could you please help me on that?
Thanks,
Here you go. Button added with auto layout constraints so it will work the same in all size classes :)
var lastButton : UIButton? = nil
for i in 0...5 {
let button = UIButton()
button.backgroundColor = UIColor.green
button.setTitle("Button \(i)", for: .normal)
button.sizeToFit()
button.translatesAutoresizingMaskIntoConstraints = false
if i == 0 {
let viewComponents : [String : Any] = ["button" : button]
self.view.addSubview(button)
let horizontalConstraint = NSLayoutConstraint.constraints(withVisualFormat: "H:|-(20)-[button]", options: [], metrics: nil, views: viewComponents)
let verticalConstraint = NSLayoutConstraint.constraints(withVisualFormat: "V:|-(8)-[button]", options: [], metrics: nil, views: viewComponents)
self.view.addConstraints(horizontalConstraint)
self.view.addConstraints(verticalConstraint)
self.view.layoutIfNeeded()
lastButton = button
}
else {
if (lastButton != nil) {
self.view.addSubview(button)
let viewComponents : [String : Any] = ["button" : button, "lastButton" : lastButton!]
if (lastButton!.frame.maxX + 8 + button.bounds.size.width) > self.view.bounds.size.width {
let horizontalConstraint = NSLayoutConstraint.constraints(withVisualFormat: "|-(8)-[button]", options: [], metrics: nil, views: viewComponents)
let verticalConstraint = NSLayoutConstraint.constraints(withVisualFormat: "V:[lastButton]-(8)-[button]", options: [], metrics: nil, views: viewComponents)
self.view.addConstraints(horizontalConstraint)
self.view.addConstraints(verticalConstraint)
self.view.layoutIfNeeded()
lastButton = button
}
else {
self.view.addSubview(button)
let horizontalConstraint = NSLayoutConstraint.constraints(withVisualFormat: "[lastButton]-(8)-[button]", options: [.alignAllTop], metrics: nil, views: viewComponents)
self.view.addConstraints(horizontalConstraint)
self.view.layoutIfNeeded()
lastButton = button
}
}
}
}
The idea is using UICollectionView. You can implement it with following:
Subclass UICollectionViewCell to make your own cell which has the button
Subclass UICollectionViewFlowLayout, override
layoutAttributesForItem(at indexPath: IndexPath) which provides the attributes for each cell. You may need to pre-calculate the frame of button or cell in function prepare()
Create UICollectionView instance with the collectionviewLayout you just subclassed.

addTarget and addGestureRecognizer not working, no crash/error

I a have an overlay with a table in and I'd like to add a Tap gesture recogniser to the background to dismiss the view and also addTarget to a button within the overlay which does the same thing.
The overlay displays fine as expected, however whenever I tap the black background or the cancel button, nothing happens. I've searched for an answer here but nothing found has worked. My code is as follows, followed by a screenshot of the overlay:
class importedFileView: NSObject {
let blackView = UIView()
let importedFileContainerView: UIView = {
let importedFileContainerView = UIView(frame: .zero)
importedFileContainerView.backgroundColor = .white
importedFileContainerView.layer.cornerRadius = 10
importedFileContainerView.layer.masksToBounds = true
return importedFileContainerView
}()
let headerLabel: UILabel = {
let headerLabel = UILabel()
headerLabel.translatesAutoresizingMaskIntoConstraints = false
headerLabel.font = UIFont(name: "HelveticaNeue-Thin" , size: 24)
headerLabel.text = "Attach file"
headerLabel.textColor = .darkGray
headerLabel.adjustsFontSizeToFitWidth = true
return headerLabel
}()
let fileTableView: UITableView = {
let fileTableView = UITableView()
return fileTableView
}()
let updateDetailsButton: UIButton = {
let updateDetailsButton = UIButton()
updateDetailsButton.translatesAutoresizingMaskIntoConstraints = false
updateDetailsButton.backgroundColor = UIColor(r:40, g:86, b:131)
updateDetailsButton.setTitleColor(UIColor.white, for: .normal)
updateDetailsButton.setTitle("Attach selected files", for: .normal)
updateDetailsButton.titleLabel!.font = UIFont(name: "HelveticaNeue-Light" , size: 18)
updateDetailsButton.layer.cornerRadius = 2
return updateDetailsButton
}()
let cancelButton: UIButton = {
let cancelButton = UIButton()
cancelButton.translatesAutoresizingMaskIntoConstraints = false
cancelButton.backgroundColor = UIColor.white
cancelButton.setTitleColor(UIColor(r:40, g:86, b:131), for: .normal)
cancelButton.setTitle("Cancel", for: .normal)
cancelButton.titleLabel!.font = UIFont(name: "HelveticaNeue-Light" , size: 18)
cancelButton.layer.cornerRadius = 2
return cancelButton
}()
let frameHeight: CGFloat = 450
func showFormView(){
if let window = UIApplication.shared.keyWindow {
blackView.backgroundColor = UIColor(white: 0, alpha: 0.5)
window.addSubview(blackView)
window.addSubview(importedFileContainerView)
importedFileContainerView.addSubview(headerLabel)
importedFileContainerView.addSubview(fileTableView)
importedFileContainerView.addSubview(updateDetailsButton)
importedFileContainerView.addSubview(cancelButton)
cancelButton.addTarget(self, action: #selector(handleDismiss), for: .touchUpInside)
blackView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleDismiss)))
layoutViews()
fileTableView.frame = CGRect(x: 30, y: window.frame.height, width: window.frame.width - 60, height: 230)
let frameY = (window.frame.height - frameHeight) / 2
importedFileContainerView.frame = CGRect(x: 20, y: window.frame.height, width: window.frame.width - 40, height: self.frameHeight)
blackView.frame = window.frame
blackView.alpha = 0
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.blackView.alpha = 1
self.importedFileContainerView.frame = CGRect(x: 20, y: frameY, width: window.frame.width - 40, height: self.frameHeight)
}, completion: nil
)
}
}
func layoutViews(){
let views = ["v0" : headerLabel, "v1": fileTableView, "v2": updateDetailsButton, "v3": cancelButton]
let leftSpace = NSLayoutConstraint.constraints(withVisualFormat: "H:|-20.0-[v0]-20.0-|", options: NSLayoutFormatOptions(), metrics: nil, views: views)
let leftSpace1 = NSLayoutConstraint.constraints(withVisualFormat: "H:|-20.0-[v1]-20.0-|", options: NSLayoutFormatOptions(), metrics: nil, views: views)
let leftSpace2 = NSLayoutConstraint.constraints(withVisualFormat: "H:|-20.0-[v2]-20.0-|", options: NSLayoutFormatOptions(), metrics: nil, views: views)
let leftSpace3 = NSLayoutConstraint.constraints(withVisualFormat: "H:|-20.0-[v3]-20.0-|", options: NSLayoutFormatOptions(), metrics: nil, views: views)
let topSpacing = NSLayoutConstraint.constraints(withVisualFormat: "V:|-20.0-[v0(40)]-20.0-[v1(230)]-20.0-[v2(50)]-10.0-[v3(50)]-10.0-|", options: NSLayoutFormatOptions(), metrics: nil, views: views)
importedFileContainerView.addConstraints(topSpacing)
importedFileContainerView.addConstraints(leftSpace)
importedFileContainerView.addConstraints(leftSpace1)
importedFileContainerView.addConstraints(leftSpace2)
importedFileContainerView.addConstraints(leftSpace3)
}
func handleDismiss() {
UIView.animate(withDuration: 0.5,
delay: 0.0,
options: .curveEaseInOut,
animations: {
self.blackView.alpha = 0
if let window = UIApplication.shared.keyWindow {
self.importedFileContainerView.frame = CGRect(x: 20, y: window.frame.height, width: window.frame.width - 40, height: self.frameHeight)
}
},
completion: { [weak self] finished in
self?.blackView.removeFromSuperview()
self?.importedFileContainerView.removeFromSuperview()
})
}
override init() {
super.init()
}
}
self.blackView.isUserInteractionEnabled = true;
is all you need to add to the blackView (UIView).
Without that, the view doesn't have any interactions enabled and so the gesture recognizer's target/action isn't triggered.
Events are ignored.
https://developer.apple.com/reference/uikit/uiview/1622577-isuserinteractionenabled
You might also want to disable it during animations.
Are you keeping a strong reference to the instance of your importedFileView while your overlays are visible? As far as I tested, all actions are silently ignored when the target is lost.
For example, this does not work:
#IBAction func someAction(_ sender: Any) {
let ifv = importedFileView()
ifv.showFormView()
//`ifv` is released at the end of this method, then your overlays are shown...
}
This works:
let ifv = importedFileView() //keep the instance as a property of your ViewController.
#IBAction func someAction(_ sender: Any) {
ifv.showFormView()
}
Programmatically generated UIViews isUserInteractionEnabled is default to true. You have no need to explicitly set it to true.
By the way, you'd better not name your non-UIView class as ...View, and better make your type names start with capital letter, which makes your code more readable to experienced Swift programmers.

UIStackView won't animate when changing the hidden property on iOS 9

I am using a Stack view to create a kind of table UI, I have 6 views in a StackView 0,2,4 are visible and 1,3,5 are hidden. When tapping one of the visible views I wish to "open" one of the views that are hidden.
I have this code that works great on iOS 10 but from some reason I can not understand it is not working well on iOS 9.
Note that if I load the views all open, the close animation will work but it won't open when setting the hidden property to false.
Here is my code -
EDIT
After some debugging looks like the view height constraint is nor recovering from the hiding, and it's frame is still height is 0.
import UIKit
class DeckView: UIView {
}
class ViewController: UIViewController {
var scrollView: UIScrollView!
var stackView: UIStackView!
override func viewDidLoad() {
super.viewDidLoad()
scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[scrollView]|", options: .alignAllCenterX, metrics: nil, views: ["scrollView": scrollView]))
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[scrollView]|", options: .alignAllCenterX, metrics: nil, views: ["scrollView": scrollView]))
stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.spacing = 0
stackView.alignment = .center
stackView.distribution = .fillProportionally
stackView.axis = .vertical
scrollView.addSubview(stackView)
scrollView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[stackView]|", options: NSLayoutFormatOptions.alignAllCenterX, metrics: nil, views: ["stackView": stackView]))
scrollView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[stackView]|", options: NSLayoutFormatOptions.alignAllCenterX, metrics: nil, views: ["stackView": stackView]))
for i in 0 ..< 8 {
let view = DeckView()
view.tag = i
view.translatesAutoresizingMaskIntoConstraints = false
view.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
view.isUserInteractionEnabled = true
if i%2 == 0 {
view.backgroundColor = UIColor.magenta
let constriant = view.heightAnchor.constraint(equalToConstant:160)
constriant.priority = 999
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.openDeck(_:))))
view.addConstraint(constriant)
} else {
view.backgroundColor = UIColor.red
let constriant = view.heightAnchor.constraint(equalToConstant:160)
constriant.priority = 999
view.addConstraint(constriant)
view.isHidden = false
}
stackView.addArrangedSubview(view)
}
}
func openDeck(_ sender:UIGestureRecognizer) {
if let view = sender.view as? DeckView,
let childView = stackView.viewWithTag(view.tag + 1) {
UIView.animate(withDuration: 0.4, animations: {
childView.isHidden = !childView.isHidden
})
}
}
}
keep the view's height priority lower than 1000(go for 999).
Do not set setHidden:true if it is already hidden(This is UIStackView's bug)
If any one stumble on this issue.
I was able to solve this issue by removing the -
stackView.distribution = .fillProportionally
I am not sure why this happened but I found that Autolayout added a height constraint named 'UISV-fill-proportionally' with a constant of 0 and greater priority then my height constraint. removing the fillProportionally fixed the issue.

Resources