Custom UIView with tapGesture - ios

I have a drawing app. Inside my VC there are five imageViews with five colors in them. I want to be able to click on the imageView and change the stroke color. It can be easily done if I repeat myself in the viewcontroller by adding gesture Recognizers to each UIImageView and have their individual "selector" function. Such as
func redTapped() {}
func blueTapped() {}
However, I want to be able to make the code more clear by creating a custom class (ColorImageView.Swift) for these ImageViews so that when I assign the class to these buttons, they automatically gets the tap gesture and my VC automatically receives the information about which one is tapped. At the moment, I can get a "imagePressed" printed out for each image that gets assigned to my class. However, I have no way of distinguishing which one were pressed. Below are my code for ColorImageView.Swift
import Foundation
class ColorImageView: UIImageView {
private func initialize() {
let touchGesture = UITapGestureRecognizer(target: self, action: #selector(ColorImageView.imagePressed(_:)))
touchGesture.numberOfTapsRequired = 1
self.userInteractionEnabled = true
self.addGestureRecognizer(touchGesture)
}
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
func imagePressed(gestureRecognizer: UITapGestureRecognizer) {
print("image pressed \(gestureRecognizer)")
}
}
My imageView names are red.png, green.png, blue.png...etc
Thanks

You can get the tag easily.It works fine.
func imagePressed(gestureRecognizer: UITapGestureRecognizer)
{
print("image pressed \(gestureRecognizer)")
let tappedImageVIew = gestureRecognizer.view as! UIImageView
print("image pressed \(tappedImageVIew.tag)")
}

Related

UITapGestureRecognizer in custom UIView doesn't work

Swift 5/Xcode 12.4
I created an xib file for my custom MarkerView - the layout's pretty simple:
- View
-- StackView
--- DotLabel
--- NameLabel
View and StackView are both set to "User Interaction Enabled" in the inspector by default. DotLabel and NameLabel aren't but ticking their boxes doesn't seem to actually change anything.
At runtime I create MarkerViews (for testing purposes only one atm) in my ViewController and add them to a ScrollView that already contains an image (this works):
override func viewDidAppear(_ animated: Bool) {
createMarkers()
setUpMarkers()
}
private func createMarkers() {
let marker = MarkerView()
marker.setUp("Some Text")
markers.append(marker)
scrollView.addSubview(marker)
marker.alpha = 0.0
}
private func setUpMarkers() {
for (i,m) in markers.enumerated() {
m.frame.origin = CGPoint(x: (i+1)*100,y: (i+1)*100)
m.alpha = 1.0
}
}
These MarkerViews should be clickable, so I added a UITapGestureRecognizer but the linked function is never called. This is my full MarkerView:
class MarkerView: UIView {
#IBOutlet weak var stackView: UIStackView!
#IBOutlet weak var dotLabel: UILabel!
#IBOutlet weak var nameLabel: UILabel!
let nibName = "MarkerView"
var contentView:UIView?
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
guard let view = loadViewFromNib() else { return }
view.frame = self.bounds
self.addSubview(view)
contentView = view
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickedMarker))
self.addGestureRecognizer(gesture)
}
func loadViewFromNib() -> UIView? {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: nibName, bundle: bundle)
return nib.instantiate(withOwner: self, options: nil).first as? UIView
}
func setUp(_ name:String) {
nameLabel.text = name
nameLabel.sizeToFit()
stackView.sizeToFit()
}
#objc private func clickedMarker() {
print("Clicked Marker!")
}
}
Other questions recommend adding self.isUserInteractionEnabled = true before the gesture recognizer is added. I even enabled it for the StackView and both labels and also tried to add the gesture recognizer to the ScrollView but none of it helped.
Why is the gesture recognizer not working and how do I fix the problem?
Edit: With Sweeper's suggestion my code now looks like this:
class MarkerView: UIView, UIGestureRecognizerDelegate {
.....
func commonInit() {
guard let view = loadViewFromNib() else { return }
view.frame = self.bounds
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickedMarker))
gesture.delegate = self
self.isUserInteractionEnabled = true
self.addGestureRecognizer(gesture)
self.addSubview(view)
contentView = view
}
.....
func gestureRecognizer(_: UIGestureRecognizer, _ otherGestureRecognizer: UIGestureRecognizer) -> Bool {
print("gestureRecognizer")
return true
}
}
"gestureRecognizer" is never printed to console.
One of the comments below the accepted answer for the question #Sweeper linked gives a good hint:
What is the size of the content view (log it)?
print(self.frame.size)
print(contentView.frame.size)
both printed (0.0, 0.0) in my app. The UITapGestureRecognizer is attached to self, so even if it worked, there was simply no area that you could tap in for it to recognize the tap.
Solution: Set the size of the UIView the UITapGestureRecognizer is attached to:
stackView.layoutIfNeeded()
stackView.sizeToFit()
self.layoutIfNeeded()
self.sizeToFit()
didn't work for me, the size was still (0.0, 0.0) afterwards. Instead I set the size of self directly:
self.frame.size = stackView.frame.size
This also sets the size of the contentView (because it's the MarkerView's child) but of course requires the size of stackView to be set properly too. You could add up the childrens' widths/heights (including spacing/margins) yourself or simply call:
stackView.layoutIfNeeded()
stackView.sizeToFit()
The finished code:
func setUp(_ name:String) {
nameLabel.text = name
//nameLabel.sizeToFit() //Not needed anymore
stackView.layoutIfNeeded() //Makes sure that the labels already use the proper size after updating the text
stackView.sizeToFit()
self.frame.size = stackView.frame.size
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.onClickStackView))
self.addGestureRecognizer(gesture)
}
Specifics:
There's no need for a delegate and overriding func gestureRecognizer(_: UIGestureRecognizer, _ otherGestureRecognizer: UIGestureRecognizer) -> Bool to always return true, which might be an alternative but I didn't manage to get that version to work - the function was never called. If someone knows how to do it, please feel free to post it as an alternative solution.
Attaching the UITapGestureRecognizer has to be done once the parent view knows its size, which it doesn't in commonInit because the text of the labels and the size of everything isn't set there yet. It's possible to add more text after the recognizer is already attached but everything that exceeds the original length (at the time the recognizer was attached) won't be clickable if using the above code.
The parent view's background color is used (the stackView's is ignored) but it's possible to simply set it to a transparent color.
Attaching the gesture recognizer to stackView instead of self works the same way. You can even use a UILabel but their "User Interaction Enabled" is off by default - enable it first, either in the "Attributes Inspector" (tick box in the "View" section) or in code (myLabel.isUserInteractionEnabled = true).

Custom UIControl element with global state for all instances

I have drawer controller presenting menu in the iOS app.
This menu is toggled by pressing menu buttons (UIButton) available on each screen.
As you can see in the mock: menu buttons can have red dot showing that new content is available - for this case I simply have two images for menu button without dot and with it.
I thought about making custom UIControl with "global" property for this dot. Is it the right way?
class MenuButton : UIButton {
static var showNotificationDot : Bool = false
}
For example you could create subclass UIButton and add observer.
class MyButton: UIButton {
static let notificationKey = NSNotification.Name(rawValue: "MyButtonNotificationKey")
override init(frame: CGRect) {
super.init(frame: frame)
self.subcribeForChangingState()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
fileprivate func subcribeForChangingState() {
NotificationCenter.default.addObserver(forName: MyButton.notificationKey, object: nil, queue: nil) { notificaton in
if let state = notificaton.object as? Bool {
self.changeState(active: state)
}
}
}
fileprivate func changeState(active: Bool) {
//change ui of all instances
print(active)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
And change UI from any place like this:
NotificationCenter.default.post(name: MyButton.notificationKey, object: true)

Using image as gesture recognizer in Swift/Xcode, image coming up nil

I just got finished going through the big nerd ranch iOS programming book and started my first 'project'. I'm trying to use a UIImageView as a button but my image is coming up as nil and I cannot figure out why. I'm new at this, so any help, or even just identifying any parts of this code that don't make sense is appreciated.
import UIKit
class ImageScreenViewController: UIViewController {
# IBOutlet var imageView: UIImageView!
// set up images
let picture1 = UIImage(named: "picture1")
override func viewDidLoad() {
super.viewDidLoad()
// set image on screen
imageView.image = picture1
}
// set up gesture recognizer
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(count(_:)))
self.imageView.addGestureRecognizer(tapRecognizer)
self.imageView.isUserInteractionEnabled = true
}
#objc func count(_ gestureRecognizer: UIGestureRecognizer) {
print("tapped the picture")
}
}
note, picture1 is in Assets.xcassets.
Some explanation for your question
encodeWithCoder(_ aCoder: NSCoder) {
// Serialize
}
init(coder aDecoder: NSCoder) {
// Deserialize
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
//You used
}
To add a Gesture you actually do not need to implement coder here as These are being used to store the state
Check following example
1- ImageView is being taken from storyBoard
2- myImage is being Added using code
Now , Storyboard initially implants serialise and deserialise mechanism
Thus imageView is a property being added from storyboard so no need for coder there
import UIKit
class ImageVC: UIViewController {
# IBOutlet var imageView: UIImageView!
// set up images
let picture1 = UIImage(named: "cc")
//ImageView programmaticlly
var myImage = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// set image on screen
imageView.image = picture1
//setting properties and frame
myImage.image = picture1
myImage.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
self.view.addSubview(myImage)
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(count(_:)))
self.imageView.addGestureRecognizer(tapRecognizer)
self.imageView.isUserInteractionEnabled = true
}
// set up gesture recognizer
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
//adding gesture
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(count(_:)))
self.myImage.addGestureRecognizer(tapRecognizer)
self.myImage.isUserInteractionEnabled = true
}
#objc func count(_ gestureRecognizer: UIGestureRecognizer) {
print("tapped the picture")
}
}
As if you implement above Algo It won't crash , as For Properties being added from storyboard can directly be accessed in didload or willAppear as we never going to remove them from superview so indirectly its serialised
Second case - myImage - Added Programmatically can also be removed from superView and added again Thus can be serialised means need to be
you can add tapRecongnize in viewDidload because in init method imageview not initialize and return nil so that you can use like this
override func viewDidLoad() {
super.viewDidLoad()
imageView.image = picture1
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(count(_:)))
self.imageView.addGestureRecognizer(tapRecognizer)
self.imageView.isUserInteractionEnabled = true
}
var image: UIImage?
#IBOutlet weak var imageChoisie: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
imageChoisie.image = image // from Segue
// Click on Image
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(count(_:)))
self.imageChoisie.addGestureRecognizer(tapRecognizer)
self.imageChoisie.isUserInteractionEnabled = true
}
#objc func count(_ gestureRecognizer: UIGestureRecognizer) {
//print("tapped the picture")
dismiss(animated: true, completion: nil) // back previous screen
}

iOS charts - draw value only when highlighted

I want to highlight and show value only when tapping on the iOS-Chart. I enabled the highlight but not the values because I only want them when tap and highlight
lineChartDataSet.drawValuesEnabled = false
lineChartDataSet.highlightEnabled = true
Do I need this function?
func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) {}
It's an old question, but I think it's still actual for some developers.
If you want to show values, baloon or highlight bar only while user is touching a chart view you may catch a touch event with UILongPressGestureRecognizer.
I instantiated new TappableLineChartView class from LineChartView. But you can work with BarChartView in the same way. Also if you don't want to instantiate new classes, you can incorporate addTapRecognizer and chartTapped functions in your view controller.
In my example, I show and hide values, but in the same manner, you can show and hide a balloon or another marker.
class TappableLineChartView: LineChartView {
public override init(frame: CGRect)
{
super.init(frame: frame)
addTapRecognizer()
}
public required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
addTapRecognizer()
}
func addTapRecognizer() {
let tapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(chartTapped))
tapRecognizer.minimumPressDuration = 0.1
self.addGestureRecognizer(tapRecognizer)
}
#objc func chartTapped(_ sender: UITapGestureRecognizer) {
if sender.state == .began || sender.state == .changed {
// show
let position = sender.location(in: self)
let highlight = self.getHighlightByTouchPoint(position)
let dataSet = self.getDataSetByTouchPoint(point: position)
dataSet?.drawValuesEnabled = true
highlightValue(highlight)
} else {
// hide
data?.dataSets.forEach{ $0.drawValuesEnabled = false }
highlightValue(nil)
}
}
}

GestureRecognizer not responding to tap

After initialisation of by subclass of UIImageView I have the following line of code:
self.userInteractionEnabled = true
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "handleTap:"))
I created the necessary associated function :
func handleTap(gestureRecognizer: UITapGestureRecognizer) {
print("In handler")
}
On tapping on the view in question, "In handler was never printed to the console". I then removed the handler function to see if the compiler would complain about the missing function. It didn't.
I'm positively stumped. I'd truly appreciate any light people can shed on this.
Update: My class is actually a UIImageView as opposed to UIView
I was using UITapGestureRecognizer that I placed on a UILabel using Storyboard.
To get this to work I also had to place a checkmark in the block labeled: "User Interaction Enabled" in the UILabel Attributes Inspector in the Storyboard.
I discovered the answer after carefully combing through my code.
One of the parent views was created without supplying a frame:
While it's a noobish enough error to warrant deletion of this questions, odds are someone else will also have the same issue in the future...
Try this
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.view.userInteractionEnabled = true
var tapGesture = UITapGestureRecognizer(target: self, action: Selector("handleTap:"))
self.view.addGestureRecognizer(tapGesture)
}
func handleTap(sender : UIView) {
println("Tap Gesture recognized")
}
In addition to the other answers, this can be caused by adding the gesture recognizer to multiple views. Gesture recognizers are for single views only.
Reference: https://stackoverflow.com/a/5567684/6543020
I ran into this problem with programmatic views.
My UIView with the gesture recognizer had .isUserInteractionEnabled = true, but it did not respond to taps until I set .isUserInteractionEnabled = true for its parent views as well.
Most likely you add UIGestureRecognizer in wrong place. Here is working sample with UIView from storyboard. If you create your UIView dynamically then you should put this initialization in the correct constructor.
class TestView: UIView
{
override func awakeFromNib()
{
self.userInteractionEnabled = true
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "handleTap:"))
}
func handleTap(gestureRecognizer: UITapGestureRecognizer)
{
println("Here")
}
}
I found the solution to this problem after lot of trial and error. So there are two solution two this
1. Either add the GestureRecognizer in viewDidLoad() and turn
userInteractionEnabled = true
2. If using computed property use lazy var instead of let to the property.
lazy var profileImageView: UIImageView = {
let iv = UIImageView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
iv.image = #imageLiteral(resourceName: "gameofthrones_splash")
iv.contentMode = .scaleAspectFill
iv.translatesAutoresizingMaskIntoConstraints = false
iv.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleSelectProfileImageView)))
iv.isUserInteractionEnabled = true
return iv
}()
For anyone that is still having problems even with all the previous answers, make sure you are using UITapGestureRecognizer instead of UIGestureRecognizer, I kept missing this detail while trying to find what was wrong.
This Code works for me with XCode 7.0.1
import UIKit
class ImageView: UIImageView {
init(frame: CGRect, sender: Bool, myImage: UIImage) {
super.init(frame: frame)
self.image = myImage
initBorderStyle(sender)
// enable user interaction on image.
self.userInteractionEnabled = true
let gesture = UITapGestureRecognizer(target: self, action: "previewImage:")
addGestureRecognizer(gesture)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func previewImage(myGesture: UITapGestureRecognizer? = nil) {
print("i'm clicked")
}
private func initBorderStyle(sender: Bool) {
self.layer.masksToBounds = true
self.layer.cornerRadius = 8
self.layer.borderWidth = 0.5
self.layer.borderColor = getBorderColor(sender)
self.backgroundColor = getColor(sender)
}
func getBorderColor(sender: Bool) -> CGColor {
var result: CGColor
if sender {
result = UIColor(red: 0.374, green: 0.78125, blue: 0.0234375, alpha: 0.5).CGColor
} else {
result = UIColor(red: 0.3125, green: 0.6015625, blue: 0.828125, alpha: 0.5).CGColor
}
return result
}
}
Xcode 11.4 Swift 5.2
UserInteraction is enabled by default on Custom UIView
import UIKit
class YourView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapped))
self.addGestureRecognizer(tapGesture)
}
#objc func tapped() {
// do something
}
}
If you want to be notified elsewhere that the custom UIView has been tapped, then it is convenient to use a Notification.
It is best to create this as an extension to prevent it being Stringly typed.
extension Notification.Name {
static let DidTapMyView = Notification.Name("DidTapMyView")
}
In your custom UIView when the tapped function is called
#objc func tapped() {
NotificationCenter.default.post(name: .DidTapMyView, object: self)
}
In your UIViewController where you want to listen for the Notification:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(myViewWasTapped), name: .DidTapMyView, object: nil)
}
#objc func myViewWasTapped() {
// Notified here - do something
}
Here's a list of my findings in 2021, XCode 13:
Make sure both your view and all of its superviews have set width & height constraints(this one is crucial)
Set isUserInteractionEnabled = true for your view
There's no need to set explicit frame or isUserInteractionEnabled for other super views
In addition the above (userInteractionEnabled, clipsToBounds, etc.), if you are working with a view in a child view controller be sure that you have added it as a child with myParentViewController.addChild(myChildViewController) — we've run into a couple of situations now where visible views were not firing recognizing gestures because their view controllers hadn't been added, and presumably the VCs themselves were not being retained.
It turned out that my gesture didn't work because it was in a normal class, and not a subclass of UIView or anything.
I solved the issue by creating a subclass of UIView that I had an instance of in this normal class and placed the gesture logic in that.
I solved my issue by setting the height to the UIView.
optionView.heightAnchor.constraint(equalToConstant: 18).isActive = true
I had the same issue in my programmatically created ViewController. The bug was fixed when I added a backgroundColor to the view.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let tap = UITapGestureRecognizer(target: self.view, action: #selector(UIView.endEditing))
view.addGestureRecognizer(tap)
}
}

Resources