I'm creating many UI elements programmatically. Let's say 50 UILabels.
What is a proper way to access to labels properties?
Now i'm adding a tag to each label and next search label in [subview] array and get label properties through a 'sender':
func buttonTapped(sender: UIButton) {
for subview in containerView.subviews {
if let label = subview as? UILabel, label.tag == sender.tag {
// do stuff
}
}
}
Not sure it's most elegant way because there are some problems if we change labels to buttons.
Lets say:
func createButton() {
let button = UIButton(frame: CGRect(origin: ...,
size: ...))
button.addTarget(self, action: #selector(buttonTapped(sender:)), for: .touchUpInside)
let longTap = UILongPressGestureRecognizer(target: self, action: #selector(disableButton(sender:)))
longTap.minimumPressDuration = 1
button.addGestureRecognizer(longTap)
.......
containerView.addSubview(button)
}
And now i can't access to properties via 'sender' in disableButton(sender:) method, because sender is UILongPressGestureRecognizer.
Seems like im doing something wrong if it works for labels but not for buttons.
Please, guide me in right direction
First of all, I would keep and array with a reference to all of the labels so you don't have to iterate through all of the subviews to find them and keep this in order with the highest z value first. But if you don't want to do that here is the code that can find you view in one line; its not the most efficient thing in the world but for your number fo views it should be fine.
class ViewController: UIViewController {
let first = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 50))
let second = UILabel(frame: CGRect(x: 25, y: 25, width: 100, height: 50))
override func viewDidLoad() {
first.text = "first"
first.backgroundColor = .blue
second.text = "second"
second.backgroundColor = .red
view.addSubview(first)
view.addSubview(second)
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(sender:)))
view.addGestureRecognizer(tap)
}
#objc func handleTap(sender: UITapGestureRecognizer) {
if let label = view.subviews.flatMap({$0 as? UILabel}).last(where: { $0.bounds.contains(sender.location(in: $0))}) {
print("You pressed: \(label)")
}
}
}
Note whats going on here:
We use flatMap to discard any subviews that are not UIlabels.
We use last to find the last item in the array (
because the top most views are last according to UIView.subviews) where the touch is inside that view's bounds.
Simple add this code when your creating your label
yourLbl.isUserInteractionEnabled = true
And use touchesBegan method to get your clicked label
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
if let lblView = touch?.view
{
if lblView.isKind(of: UILabel.self)
{
let yourLbl = lblView as! UILabel
//Access your clicked lable
}
}
}
Related
I have developed a UIView from For loop and basically it is create 3 Views from loop. and I have to add touch gesture on every View to call a method but I am unable to get current selected UIView.tag when I tap on it. it is only showing the .tag of the last view. here is my code.
for i in 0 ... 2 {
let productView = UIView()
productView.tag = i
productView.isUserInteractionEnabled = true
let producttap = UITapGestureRecognizer(target: self, action: #selector(self.ProductTapped))
productView.addGestureRecognizer(producttap)
productView.frame = CGRect(x: xOffset, y: CGFloat(buttonPadding), width: 200, height: scView1.frame.size.height)
xOffset = xOffset + CGFloat(buttonPadding) + productView.frame.size.width
scView1.addSubview(productView)
productIndex = productView.tag
}
and here is the method that I am calling from every UIView touch.
#objc func ProductTapped() {
print("",productIndex)
}
Your code should be using delegate/callback closure, but if you want to keep using tag, try change it to:
#objc func ProductTapped(_ sender: UITapGestureRecognizer) {
if let view = sender.view {
print(view.tag)
}
}
and the gesture attach to let producttap = UITapGestureRecognizer(target: self, action: #selector(self.ProductTapped(_:)))
productIndex does nothing here since it got overwritten on the loop
productIndex currently has no relationship to the tap gestures that you attach your views. You do set productIndex in the the loop but that's irrelevant to your gesture.
Perhaps you want
let producttap = UITapGestureRecognizer(target: self, action: #selector(productTapped(_:))
and
#objc func productTapped(_ gesture: UITapGestureRecognizer) {
print("tag is",gesture.view.tag)
}
I am trying to add a delete button as a subview in an image. This is my current structure:
-> class DesignViewController: UIViewController
|
-> class Sticker: UIImageView, UIGestureRecognizerDelegate
|
-> UI button inside the Sticker
Inside Sticker class I have :
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let button2 = UIButton(frame: CGRect(x: 100, y: 100, width: 100, height: 50))
button2.backgroundColor = .red
button2.setTitle("Delete", for: .normal)
button2.tag = 23
button2.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
self.addSubview(button2)
}
#objc func buttonAction(sender: UIButton!) {
print("Button tapped")
}
The buttonAction is not getting called.
When I change self.addSubview(button2) line to :
self.superview?.addSubview(button2)
I can see buttonAction getting called. However I would like to keep the button inside the Sticker view so that when user moves the sticker, the button moves as a subview with it.
Can anyone please help and let me know how I can keep the button inside Sticker view?
By default isUserInteractionEnabled property of UIImageView is set to false. Set it to true and your button will start to respond. You can set it in code as well as in the storyboards.
Also try setting the clipsToBounds property of your imageview to true. It will clip your button if it is going outside of the image bounds. That might be one of the reason that your button is not getting touches.
You should create a protocol delegate for button action. This is code example:
protocol ButtonDelegate: class {
func buttonTapped(button: UIButton)
}
class Sticker: UIImageView {
weak var delegate: ButtonDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(button2)
}
lazy var button2: UIButton = {
let button = UIButton(frame: CGRect(x: 100, y: 100, width: 100, height: 50))
button2.backgroundColor = .red
button2.setTitle("Delete", for: .normal)
button2.tag = 23
button2.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
return button
}()
#objc func buttonAction(sender: UIButton) {
guard let delegate = delegate else { return }
delegate.buttonTapped(button: sender)
}
So, now go to your DesignViewControllerl, add your custom imageview class Sticker. Don't forget to do that "imageView.delegate = self". Then in extension add protocol delegate you've created before. Code example:
class DesignViewController: UIViewController {
private lazy var sticker: Sticker = {
let iv = Sticker(frame: view.bounds)
iv.delegate = self
return iv
}()
override viewDidLoad() {
super.viewDidLoad()
view.addSubiew(sticker)
}
}
extension DesignViewController: ButtonDelegate {
func buttonTapped(button: UIButton) {
// input your action here
}
}
I am trying to utilise panGestureRecogniser to enable users of my first app to move programmatically created textfields. Having searched for the solution I only found outdated code, for IBOutlets. The problem is that I struggle with setting the position of newly created textfields to the position that the users drags to. Everything is working just fine, when I test my code on a textfield create in a storyboard, connected via an IBOutlet. My problem is how to make a connection between the location from the didPan function and any new label created by generateTextField func. Another thing is that I want mu users to be able to create many textfields and each should be draggable, so any solution should include that. I paste my code below:
override func viewDidLoad() {
super.viewDidLoad()
}
func generateTextField() {
let textPanel = UITextField(frame: CGRect(x: 50, y: 50, width: 100, height: 30))
textPanel.textAlignment = NSTextAlignment.center
textPanel.textColor = UIColor.blue
textPanel.borderStyle = UITextBorderStyle.line
self.view.addSubview(textPanel)
let gest = UIPanGestureRecognizer(target: self, action: #selector(didPan))
textPanel.isUserInteractionEnabled = true
textPanel.addGestureRecognizer(gest)
}
#objc func didPan(sender: UIPanGestureRecognizer){
let location = sender.location(in: view)
if sender.state == .began {
print("Gesture began")
} else if sender.state == .changed {
print("Gesture is changing")
} else if sender.state == .ended {
print("Gesture ended")
}
}
#IBAction func generateTextFieldPressed(_ sender: Any) {
generateTextField()
}
In didPan you can retrieve textField from recognizer like:
let textField = sender.view as TextField
And after that change textField's frame.
I have two labels, Label1 and Label2. I want to make a single function that prints out which label is tapped by creating UITTapRecognizer for both labels calling the same function with selector that passes an argument. Below is the long way of doing it which is messy but works. If I know how to pass an argument (Int) into the selector, it would be alot cleaner.
let topCommentLbl1Tap = UITapGestureRecognizer(target: self, action: #selector(DiscoverCell().doubleTapTopComment1))
topCommentLbl1Tap.numberOfTapsRequired = 2
topCommentLbl1.userInteractionEnabled = true
topCommentLbl1.addGestureRecognizer(topCommentLbl1Tap)
let topCommentLbl2Tap = UITapGestureRecognizer(target: self, action: #selector(DiscoverCell().doubleTapTopComment2))
topCommentLbl2Tap.numberOfTapsRequired = 2
topCommentLbl2.userInteractionEnabled = true
topCommentLbl2.addGestureRecognizer(topCommentLbl2Tap)
func doubleTapTopComment1() {
print("Double Tapped Top Comment 1")
}
func doubleTapTopComment2() {
print("Double Tapped Top Comment 2")
}
Is there a way to modify the selector method such that I can do something like
func doubleTapTopComment(label:Int) {
if label == 1 {
print("label \(label) double tapped")
}
Short answer: no
The selector is called by the UITapGestureRecognizer, and you have no influence on what parameters it passes.
However, what you can do is query the recognizer's view property to get the same information.
func doubleTapComment(recognizer: UIGestureRecognizer) {
if recognizer.view == label1 {
...
}
else if recognizer.view == label2 {
...
}
}
Provide both gesture recognizers with the same selector that takes a single parameter. That action method will be passed instance of a UIGestureRecognizer, and happily, that gesture recognizer has a property called view, which is the view to which the gr is attached.
... action: #selector(doubleTapTopComment(_:))
func doubleTapTopComment(gestureRecognizer: gr) {
// gr.view is the label, so you can say gr.view.text, for example
}
I believe a UITapGestureRecognizer can only be used for a single view. That said, you can have 2 different UITapGestureRecognizers call the same selector and then access the UITapGestureRecognizer in the function. See the following code:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let label1 = UILabel()
label1.backgroundColor = UIColor.blueColor()
label1.frame = CGRectMake(20, 20, 100, 100)
label1.tag = 1
label1.userInteractionEnabled = true
self.view.addSubview(label1)
let label2 = UILabel()
label2.backgroundColor = UIColor.orangeColor()
label2.frame = CGRectMake(200, 20, 100, 100)
label2.tag = 2
label2.userInteractionEnabled = true
self.view.addSubview(label2)
let labelOneTap = UITapGestureRecognizer(target: self, action: #selector(ViewController.whichLabelWasTapped(_:)))
let labelTwoTap = UITapGestureRecognizer(target: self, action: #selector(ViewController.whichLabelWasTapped(_:)))
label1.addGestureRecognizer(labelOneTap)
label2.addGestureRecognizer(labelTwoTap)
}
Both UITapGestureRecognizers call the same function:
func whichLabelWasTapped(sender : UITapGestureRecognizer) {
//print the tag of the clicked view
print (sender.view!.tag)
}
If you try to add one of the UITapGestureRecognizers to both labels, then only the last one added will actually call the function.
You can do like this,
let topCommentLbl1Tap = UITapGestureRecognizer(target: self, action: #selector(labelTapped(_:)))
let topCommentLbl2Tap = UITapGestureRecognizer(target: self, action: #selector(labelTapped(_:)))
label1.addGestureRecognizer(topCommentLbl1Tap)
label2.addGestureRecognizer(topCommentLbl2Tap)
#objc
func textViewTapped(_ sender: UITapGestureRecognizer) {
if(sender.view.tag == label1.tag) {
print("I am label1")
} else if(sender.view.tag == label2.tag) {
print("I am label2")
}
}
don't forgot to set tags to labels.
I am trying to create a custom UIView/Scrollview named MyScrollView that contains a few labels (UILabel), and these labels receive tap gestures/events in order to respond to user's selections .
In order to make the tap event work on the UILabels, I make sure they all have userIteractionEnabled = true and I created a delegate as below:
protocol MyScrollViewDelegate {
func labelClicked(recognizer: UITapGestureRecognizer)
}
The custom UIView is being used in ScrollViewController that I created, this ScrollViewController implements the delegate method as well:
import UIKit
import Neon
class ScrollViewController: UIViewController, MyScrollViewDelegate {
var curQuestion: IPQuestion?
var type: QuestionViewType?
var lastClickedLabelTag: Int = 0 //
init(type: QuestionViewType, question: IPQuestion) {
super.init(nibName: nil, bundle: nil)
self.curQuestion = question
self.type = type
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.automaticallyAdjustsScrollViewInsets = false
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func loadView() {
view = MyScrollView(delegate: self, q: curQuestion!)
view.userInteractionEnabled = true
}
}
// implementations for MyScrollViewDelegate
extension ScrollViewController {
func labelTitleArray() -> [String]? {
print("labelTitleArray called in implemented delegate")
return ["Comments", "Answers"]
}
func labelClicked(recognizer: UITapGestureRecognizer) {
print("labelClicked called in implemented delegate")
let controller = parentViewController as? ParentViewController
controller?.labelClicked(recognizer)
lastClickedLabelTag = recognizer.view!.tag
}
}
// MARK: - handle parent's ViewController event
extension QuestionDetailViewController {
func updateActiveLabelsColor(index: Int) {
print("updating active labels color: \(index)")
if let view = view as? MyScrollView {
for label in (view.titleScroll.subviews[0].subviews as? [UILabel])! {
if label.tag == index {
label.transform = CGAffineTransformMakeScale(1.1,1.1)
label.textColor = UIColor.purpleColor()
}
else {
label.transform = CGAffineTransformMakeScale(1,1)
label.textColor = UIColor.blackColor()
}
}
}
}
}
This above ScrollViewController is added, as a child view controller to the parent view controller, and positioned to the top part of the parent's view:
override func viewDidLoad() {
super.viewDidLoad()
self.automaticallyAdjustsScrollViewInsets = false
self.view.backgroundColor = UIColor.whiteColor()
addChildViewController(scrollViewController) // added as a child view controller here
view.addSubview(scrollViewController.view) // here .view is MyScrollView
scrollViewController.view.userInteractionEnabled = true
scrollViewController.view.anchorToEdge(.Top, padding: 0, width: view.frame.size.width, height: 100)
}
The app can load everything up in the view, but the tap gesture/events are not passed down to the labels in the custom MyScrollView. For this, I did some google search and have read Event Delivery: Responder Chain on Apple Developer website and did a hit test as well. The hitTest function below can be triggered in the MyScrollView:
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
print("hit test started, point: \(point), event: \(event)")
return self
}
My observations with the hitTest is that the touchesBegan() and touchesEnded() methods are triggered in the view only when the hitTest function is there. Without hitTest, both functions do not get called with taps.
but no luck getting the UILabel to respond to Tap Gestures. So I am reaching out to experts on SO here. Thanks for helping!
I think I found out the reason why the UILabel did not respond to tapping after much struggle: the .addGestureRecognizer() method to the label was run in the init() method of my custom UIView component, which is wrong, because the view/label may not have been rendered yet. Instead, I moved that code to the lifecycle method layoutSubviews(), and everything started to work well:
var lastLabel: UILabel? = nil
for i in 0..<scrollTitleArr.count {
let label = UILabel()
label.text = scrollTitleArr[i] ?? "nothing"
print("label: \(label.text)")
label.font = UIFont(name: "System", size: 15)
label.textColor = (i == 0) ? MaterialColor.grey.lighten2 : MaterialColor.grey.darken2
label.transform = (i == 0) ? CGAffineTransformMakeScale(1.1, 1.1) : CGAffineTransformMakeScale(0.9, 0.9)
label.sizeToFit()
label.tag = i // for tracking the label by tag number
label.userInteractionEnabled = true
label.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.labelClicked(_:))))
titleContainer.addSubview(label)
if lastLabel == nil {
label.anchorInCorner(.TopLeft, xPad: 0, yPad: 0, width: 85, height: 40)
// label.anchorToEdge(.Left, padding: 2, width: 85, height: 40)
} else {
label.align(.ToTheRightMatchingTop, relativeTo: lastLabel!, padding: labelHorizontalGap, width: 85, height: 40)
}
lastLabel = label
}
In addition, I don't need to implement any of the UIGestureRecognizer delegate methods and I don't need to make the container view or the scroll view userInteractionEnabled. More importantly, when embedding the custom UIView to a superview, I configured its size and set clipsToBounds = true.
I guess I should have read more UIView documentation on the Apple Developer website. Hope this will help someone like me in the future! Thanks to all!
You have to set the property userInteractionEnabled = YES.
For some reason, my simulator was frozen or something when the tap gesture recognizer wasn't working. So, when I restarted the app, then it all worked again. I don't know if this applies here, but that was the fix for me.