find descendant buttons in nested container - swift

I have placed buttons in multiple nested StackViews
I then placed the views within a CollectionView (yes I recognize this isn't necessarily the best or optimal way to do this).
I'm trying to figure out how I can programmatically find all my buttons.
If I do this, all I get is my StackView instead of getting the buttons (which are now 3rd level descendants). I'd love to not have to right a nested loop and instead find a function that helps me find all descendants of type UIButton.
for subview in buttonContainer.subviews {
let button = subview as? UIButton
if button != nil {
button!.setTitle("TEST", for: UIControl.State.normal)
}
}
The reason I'm doing this is just as an exercise to learn different methods, which is why I am ok nesting multiple stack views and then placing them within a CollectionView

You need to create a recursive method like:
func buttonsIn(_ view: UIView) {
if let button = view as? UIButton {
button.setTitle("TEST", for: .normal)
} else {
view.subviews.forEach({ buttonsIn($0) })
}
}
Usage:
buttonsIn(view)
Modify the recursive method as per your requirement.

I didn't test it but using recursion you should be able to accomplish this. Something like this may work for you:
func findButtonsIn(_ view: UIView) -> [UIButton] {
var buttons: [UIButton] = []
view.subviews.forEach({
if let button = $0 as? UIButton {
buttons.append(button)
} else {
buttons.append(contentsOf: self.findButtonsIn($0))
}
})
return buttons
}
Usage
findButtonsIn(yourView)
The function will return all found UIButtons as an array.
You could use
self.findButtonsIn(UIView()).enumerated().forEach({
$0.element.setTitle("\($0.offset)", for: .normal)
})
for example to set the buttons title according to the order they were found in.

Related

Swift dropDown library

I am using the dropdown library. When you press a button, you want to develop it in a format where text changes on the button when you click an element.I can print but I don't know how to change the text of the button. And I want to change the sort order of the table view when I click that element. How should I do it?
func dropDown() {
sequenceDropDown.dataSource = ["Latest order
", "Priority"]
sequenceDropDown.show()
sequenceDropDown.anchorView = sequence
sequenceDropDown.bottomOffset = CGPoint(x: 0, y:(sequenceDropDown.anchorView?.plainView.bounds.height)!)
sequenceDropDown.selectedTextColor = .white
sequenceDropDown.selectionBackgroundColor = .mainColor
}
This is dropDown function.
output.loadData.bind(to: tableView.rx.items(cellIdentifier: "mainCell", cellType: MainCell.self)) { row, element, cell in
cell.nameLbl.text = element.nickname
cell.titleTxtField.text = element.title
cell.contentTxtView.text = element.description
cell.postImage.image = UIImage(named: element.media)
cell.timeLbl.text = element.date
cell.sirenBtn.rx.tap.subscribe(onNext: { _ in
self.selectIndexPath.accept(row)
}).disposed(by: self.disposeBag)
}.disposed(by: disposeBag)
This is binding tableview code.
setting button's title is easy myButton.setTitle("Play", for: .normal)
if you want to change sort of table view, you probably should map loadData observable to needed sort method, maybe it's even better to store ouput from loadData to some array and wrap it to observable or subject, bind it to table view and if you need to see sorted tableview just sort the array
(maybe I didn't get you case point completely)

UIButton Not Clickable when added UITabbar

When added UIButton on UITabbar to middle as shown in figure.
The button action on above the UITabBar unable to click
func setupMiddleButton() {
plusButton = UIButton(frame: CGRect(x: 0, y: 0, width: 64, height: 64))
var menuButtonFrame = plusButton.frame
menuButtonFrame.origin.x = tabBar.bounds.width/2 - menuButtonFrame.size.width/2
let hasNotched :Bool? = UIDevice.current.hasNotch
if hasNotched != nil {
menuButtonFrame.origin.y = tabBar.bounds.height - menuButtonFrame.height - 15
} else {
menuButtonFrame.origin.y = tabBar.bounds.height - menuButtonFrame.height - 50
}
plusButton.frame = menuButtonFrame
plusButton.setTitle("+", for: .normal)
plusButton.titleLabel?.font = UIFont.helveticaNeue(ofSize: 40)
plusButton.backgroundColor = UIColor.init(hexString: "5E71FE")
plusButton.titleEdgeInsets = UIEdgeInsets(top: 0,left: 10,bottom: 10,right: 10)
tabBar.addSubview(plusButton)
plusButton.layer.cornerRadius = menuButtonFrame.height/2
plusButton.addTarget(self, action: #selector(plusButtonAction(sender:)), for: .touchUpInside)
}
You need to override the hitTest method in your custom tab bar class like this
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
{
guard !clipsToBounds && !isHidden && alpha > 0 else { return nil }
for member in subviews.reversed()
{
let subPoint = member.convert(point, from: self)
guard let result = member.hitTest(subPoint, with: event)
else { continue }
return result
}
return nil
}
Basically the problem is that upper part is not clickable because it is outside of the bounds of main content view of tab bar.
This method will check if the tap is inside the bounds of the view, if it is it will return the view and the action for that button will get called.
Documentation by apple: Link
P.s I was facing the same issue recently and got this help which worked smooth.
I suspect that what you are trying to do is not possible, or at the least, not supported by Apple. (And thus not recommended since you might find a way to make it might work today but not in some future OS version.)
As a rule, Apple does not support you adding custom view objects to system components like tab bars, navigation bars, stack views, table/collection view controllers, etc except through a documented API.
I would suggest NOT doing what you are trying to do. instead, add a button in the content view of the tab bar controller. I don't know if you'll be able to make it partly cover the tab bar like you are trying to do however.
Add the button to the view of the UITabbarController instead of adding to the TabBar. And then reposition the button, it will work.

What does UISearchBar.value for key do in swift?

When using a UITableView and UISearchBar in swift, I was trying to find a way to keep the cancel button on the search bar enabled when the user searches something then scrolls in the table view, so they don't have to click twice to cancel the search. The default behaviour for resigning the first responder (or ending editing on the search bar) will gray out the cancel button, but I wanted to keep it enabled, so on Stackoverflow I found how to do this, but I can't find an answer online as to what searchBar.value(forKey: "cancelButton") does in the code below. Obviously it's somehow creating a reference to the cancel button on the search bar, but I don't understand what .value does (as I'm new to swift), and where the "cancelButton" key is coming from. I have read the func value(forKey key: String) documentation, but I still didn't understand it. It would be great if someone could explain what this is doing.
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
searchBar.resignFirstResponder()
// If the user scrolls, keep the cancel button enabled
if let cancelButton = searchBar.value(forKey: "cancelButton") as? UIButton { // <-- This line
if searchBar.text != "" { cancelButton.isEnabled = true }
else { searchBar.showsCancelButton = false }
}
}
Thanks in advance.
UISearchBar is a subclass of
UIView -> UIResponder -> NSObject
And all NSObjects are conforming the NSKeyValueCoding Protocol Reference
valueForKey: is a KVC method. It works with ANY NSObject class and anything else that conforms to the above protocol. valueForKey: allows you to access a property using a string for its name. So for instance, if I have an Account class with a property number, I can do the following:
let myAccount = Account(number: 12)
myAccount.value(forKey: "number")
Since it is a runtime check, it can't be sure what the return type will be. So you have to cast it manually like:
let number = myAccount.value(forKey: "number") as? Int
I'm not going to explain the downcast and optionals here
So you can access any property of an object that conforms to NSKeyValueCoding just by knowing its method's exact name (that can be found easily by a simple reverse engineering).
Also, there is a similar method called performSelector that lets you execute any function of the object
⚠️ But be aware that Apple will reject your app if you touch a private variable or function of a system. (If they found out!)
⚠️ Also, be aware that any of these can be renamed without notice and your app will face undefined behaviors.
I was running into the same problem as you for searching through a list but I realized you can implement UITextfields instead...
Using didReturn to do textfield.resignFirstResponder() and when it resigns to take the value using textfield.value to search through a list.
In searchbar for iOS 12 or below, to access the elements you can use key value to access the elements. Like this function -
private func configureSearchBar() {
searchBar.barTintColor = Color.navBarColor
searchBar.makeRounded(cornerRadius: 5, borderWidth: 1, borderColor: Color.navBarColor)
searchBar.placeholder = searchBarPlaceholderText
searchBar.setImage(Images.search_white, for: .search, state: .highlighted)
searchBar.setImage(Images.green_check, for: .clear, state: .normal)
if let textField = searchBar.value(forKey: "searchField") as? UITextField {
textField.textColor = .white
textField.attributedPlaceholder = NSAttributedString(string: textField.placeholder ?? "", attributes: [NSAttributedString.Key.foregroundColor: UIColor.white])
let leftSideImage = textField.leftView as? UIImageView
leftSideImage?.tintColor = .white
}
if let cancelButton = searchBar.value(forKey: "cancelButton") as? UIButton {
cancelButton.setTitleColor(.white, for: .normal)
}
}
Here by using the keys we are accessing the elements of the searchbar.

Swift: addGestureRecognizer not work for stackview children

Codes:
for ... {
let view = CategoryClass.createMyClassView()
view.myLabel.text = packTitle
view.twoLabel.text = packText
view.bgCaategory.layer.cornerRadius = 30
i = i + 1
if(i == 1){
selectPackId = packId!;
view.imgSelect.image = UIImage(named: "selected")
} else {
view.imgSelect.image = UIImage(named: "select")
}
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleSendData(sender:))))
self.stackView.addArrangedSubview(view)
}
#objc func handleSendData(sender: UITapGestureRecognizer) {
print("H 1")
}
If i click on view, nothing print "H 1"
I want if i click on view, get id or another value of view
If adding isUserInteractionEnabled as suggested by Marcel still doesn't work, also make sure that every parent view in the hierarchy has a valid frame (you can check it in Debug View Hierarchy).
E.g. it happened to me to add a UIStackView into a parent UIView but the layout constraints were not correct, so I ended up having the parent UIView frame size as 0 (but the UIStackView was still visible).
If you create the UIStackView in interface builder, the isUserInteractionEnabled property is false by default. This means that the view and all it's child views won't respond to user interaction.
When you create a view in code, this property is true be default.
Add:
stackView.isUserInteractionEnabled = true
You only have to add this once, in your viewDidLoad for example.
The reason it doesn’t work is possibly a wrong method signature. The correct signature for recognizer actions is this:
recognizerAction(_ recognizer: UIGestureRecognizer)

Load data in new view when button is clicked with swift

I am developing an app, where I create buttons programmatically. When I click a button, it will request data from a database and show it in another view.
I use button.tag to determine what data to request, and I can only get the tag once the button is clicked.
However when I click the button, it shows nothing the first time. I must click it again to see the data which I want.
override func viewDidLoad() {
super.viewDidLoad()
//parseJSON(tagId)
createButton()
// Do any additional setup after loading the view, typically from a nib.
}
func createButton(){
var j:CGFloat=60
for var i:Int = 0 ; i < myImages.count;i = i+1 {
let myButton = UIButton()
myButton.setImage(UIImage(named: "carasusto.jpg"), forState: UIControlState.Normal)
myButton.setTitleColor(UIColor.blueColor(), forState: .Normal)
myButton.frame = CGRectMake(j, j+60, 50, 50)
myButton.tag = i //assign a tag to every button
myButton.addTarget(self, action: "segueToCreate:", forControlEvents: UIControlEvents.TouchUpInside)
self.view.addSubview(myButton)
j=j+60
print(myImages[i])
}
}
and
#IBAction func segueToCreate(sender: UIButton){
tagId = String(sender.tag)//tagId needs to fetch the information
parseJSON(tagId)
performSegueWithIdentifier("segueToView", sender:self)
}
func parseJSON(tagID:String){
Alamofire.request(.GET, "http://smarttags-001-site1.btempurl.com/SmartTagsRequests.aspx", parameters: ["AjaxFunc":"GetTagAttr","TagID":"\(tagID)"]).validate().responseJSON{ response in
switch response.result{
case .Success:
if let value = response.result.value {
let json = JSON(value)
print("JSON: \(json)")
self.TagName = json[0]["TagName"].stringValue
NSLog("\(self.TagName)")
self.ContentTitle = json[0]["ContentTitle"].stringValue
NSLog("\(self.ContentTitle)")
}
case .Failure(let error):
print(error)
}enter code here
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var ViewTest : ViewTwo = segue.destinationViewController as! ViewTwo
var TagNameLabel = UILabel()
TagNameLabel.frame = CGRectMake(74, 112, 182, 64)
ViewTest.view.addSubview(TagNameLabel)
TagNameLabel.text = TagName
var ContentTitleLabel = UILabel()
ContentTitleLabel.frame = CGRectMake(74, 180, 182, 64)
ViewTest.view.addSubview(ContentTitleLabel)
ContentTitleLabel.text = ContentTitle
}
}
To follow up to MirekE's answer, here are some other steps you may want to consider:
Consider using Auto Layout in place of hard-coded frames, so your UI would adapt to different size classes.
Consider alternate approaches for showing an interactive list (of images) instead of programmatically adding buttons. For example, you could use a prototype (table view or collection view) cell. Cells are selectable and can take the place of a button. Other benefits include:
A scrollable container, should you have more buttons than would fit on screen.
A single segue (or selection) is handled by the storyboard prototype cell, instead of needing to make each programmatic "button" perform a segue (or action). Since you'd know which cell was selected, you'd no longer need a tag to figure that out.
Consider passing parameters to your destination view controller, instead of trying to instantiate and create controls in prepareForSegue. The destination's view is not loaded at that point.
Consider allowing the UI to feel more responsive, such as by performing the segue and showing placeholder details which you can then update once the network request completes. Otherwise, the user may have to wait for the network response before the segue occurs (which might make them think nothing happened and needlessly tap again, leading to an additional network request).
In general, Apple (or someone else) has already provided some way to do what you want, ideally leading to less code that you have to write, debug, test, and maintain to accomplish what you want your app to do.
Most likely cause of the problem is your call to parseJson followed by prepareForSegue. parseJson is asynchronous and you don't get data back from your service before prepareForSegue is called. As a first step, move the prepareForSegue to the completion block of the parseJson.