I have a UIButton and a UIView. The View sits above the button, and is larger than the button in size. The view itself is what I want to have accept the touch events, and I'd like to forward them to the button, so that all the normal button visual changes on touch happen.
I can't seem to make this work-- my UIView implements touchesBegan/Moved/Ended/Cancelled, and turns around and calls the same methods on the button with the same arguments. But the button doesn't respond.
I have ensured that the touch methods are in fact being called on the UIView.
Is there something obvious I'm missing here, or a better way of getting the control messages across? Or is there a good reason why this shouldn't work?
Thanks!
[Note: I'm not looking for workarounds for the view+button design. Let's assume that that's a requirement. I'm interested in the notion of controls that are touch proxies for other controls. Thanks.]
Create a ContainerView that contains a button and override hitTest:withEvent: so that it returns the UIButton instance.
Are you sure your methods are being called? If you haven't set userInteractionEnabled=YES, then it won't work.
I've used such touch-forwarding before without problems, though, so I don't know why you're seeing the problems you're seeing.
I use the small class below when I need a larger touch target than the visual size of the button.
Usage:
let button = UIButton(...) // set a title, image, target/action etc on the button
let insets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
let buttonContainer = TouchForwardingButtonContainer(button: button, insets: insets) // this is what you add to your view
// a container view for a button that insets the button itself but forwards touches in the inset area to the button
// allows for a larger touch target than the visual size of the button itself
private class TouchForwardingButtonContainer: UIView {
private let button: UIButton
init(button: UIButton, insets: UIEdgeInsets) {
self.button = button
super.init(frame: .zero)
button.translatesAutoresizingMaskIntoConstraints = false
addSubview(button)
let constraints = [
button.topAnchor.constraint(equalTo: topAnchor, constant: insets.top),
button.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -insets.bottom),
button.leftAnchor.constraint(equalTo: leftAnchor, constant: insets.left),
button.rightAnchor.constraint(equalTo: rightAnchor, constant: -insets.right)
]
NSLayoutConstraint.activate(constraints)
}
required init?(coder aDecoder: NSCoder) {
fatalError("NSCoding not supported")
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return button
}
}
Related
How can I catch any touch up event in my application view without affecting any other event in subview or subview of the subview?
Currently whenever I add UILongPressGestureRecognizer to my root view, all other addTarget functions break in subviews and their subviews.
func gestureRecognizer(_: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith: UIGestureRecognizer) -> Bool {
return true
}
override func viewDidLoad() {
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 50))
button.setTitle("Click me", for: .normal)
button.addTarget(self, action: #selector(buttonClick), for: .touchDown)
self.view.addSubview(button)
initLongTapGesture() // This kills the button click handler.
}
func initLongTapGesture () {
let globalTap = UILongPressGestureRecognizer(target: self, action: #selector(tapHandler))
globalTap.delegate = self
globalTap.minimumPressDuration = 0
self.view.addGestureRecognizer(globalTap)
}
#objc func tapHandler(gesture: UITapGestureRecognizer) {
if ([.ended, .cancelled, .failed].contains(gesture.state)) {
print("Detect global touch end / up")
}
}
#objc func buttonClick() {
print("CLICK") // Does not work when initLongTapGesture() is enabled
}
The immediate solution to allow the long press gesture to work without preventing buttons from working is to add the following line:
globalTap.cancelsTouchesInView = false
in the initLongTapGesture function. With that in place you don't need the gesture delegate method (which didn't solve the issue anyway).
The big question is why are you setting a "long" press gesture to have a minimum press duration of 0?
If your goal is to monitor all events in the app then you should override the UIApplication method sendEvent. See the following for details on how to subclass UIApplication and override sendEvent:
Issues in overwriting the send event of UIApplication in Swift programming
You can also go through these search results:
https://stackoverflow.com/search?q=%5Bios%5D%5Bswift%5D+override+UIApplication+sendEvent
Another option for monitoring all touches in a given view controller or view is to override the various touches... methods from UIResponder such as touchesBegan, touchesEnded, etc. You can override these in a view controller class, for example, to track various touch events happening within that view controller.
Unrelated but it is standard to use the .touchUpInside event instead of the touchDown event when handing button events.
I have a main NSView with 2 NSView subviews containing each a custom NSButton (all created in Interface Builder). My custom button class is programmatically creating an NSTextField below the buttons, by adding it from the superview:
And the custom NSButton class code:
class buttonUpDown: NSButton {
var myLabel:NSTextField!
required public init?(coder: NSCoder) {
super.init(coder: coder)
let labelWidth=CGFloat(90)
print("init")
superview?.autoresizesSubviews = false
myLabel = NSTextField(frame: NSMakeRect(frame.origin.x+frame.size.width/2-labelWidth/2,frame.origin.y-20,labelWidth,16))
myLabel?.stringValue = "Mesh Quality"
myLabel.alignment = .center
myLabel.font? = .systemFont(ofSize: 8)
myLabel.backgroundColor = .red
myLabel.isBordered = false
myLabel.isEditable = false
superview!.addSubview(myLabel)
}
}
When I have a single Button referencing the class, the label appears properly.
But when I have 2, only 1 label is appearing randomly left or right:
What do I miss to have a text displayed below each button ?
Found the issue. Creating a label in the init function will give erratic positioning of the view frame. Adding them from the draw func, makes it more reliable.
I wrote the following code to put a draggable view on WKWebView.
With this code, I expected a "+" icon will be displayed nearby a cursor when I dragged a file to the view.
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let rect = NSRect(x: 0, y: 0, width: 200, height: 200)
let webView = WKWebView(
frame: rect,
configuration: WKWebViewConfiguration())
webView.load(URLRequest(
url: URL(string: "https://i.imgur.com/D5ru3Q7.jpg")!))
let draggableView = DraggableView(frame: rect)
draggableView.registerForDraggedTypes([.fileURL])
self.view.addSubview(webView)
self.view.addSubview(draggableView)
}
}
class DraggableView: NSView {
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
return .copy
}
}
The result is here:
Sometimes the cursor changes to a magnifying glass (same as a mouse over behavior on WKWebView).
And sometimes "+" icon is shown.
I think the webView prevents the cursor changing.
So I tried the followings. But I couldn't fix.
Overriding hitTest of the webView worked only for non-dragging mouse over actions. But not for dragging.
webView.unregisterDraggedTypes() didn't work.
Is there anyway to fix this?
Found a solution:
for t in webView.trackingAreas {
webView.removeTrackingArea(t)
}
I think removing tracking areas on a web view is not a good idea.
But at least it satisfies my need.
I am working through the "Start Developing iOS Apps" tutorial provided by Apple, and having a problem in the Implement a Custom Control section. I have tried everything to get the button to print something to the console, but can't get it to work. I am on the section in the tutorial "Add Buttons to the View" about a fifth of the way down the page.
Everything else works fine.
I have set up the RatingControl.swift file as follows:
import UIKit
class RatingControl: UIView {
// MARK: Initialisation
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Create a square button
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
// Set the background colour of the button
button.backgroundColor = UIColor.darkGray
button.addTarget(self, action: #selector(RatingControl.ratingButtonTapped(_:)), for: .touchDown)
// Add the button to the view
addSubview(button)
}
// MARK: Button Action
func ratingButtonTapped(_: UIButton) {
print("Button pressed")
}
}
A few things there are slightly different to the tutorial, but that is because Xcode says things have changed.
In my Main.storyboard file I have added a View using the RatingControl class. The button displays but nothing happens when I click it.
Any help would be appreciated. If any extra information is needed please let me know.
There is actually a hierarchy of three views in this story:
A stack view (not mentioned in the original question)
The RatingControl
The button
Well, the stack view, a complicated control whose job involves a lot of voodoo with the constraints of its arranged subviews, is reducing the size of the RatingControl to zero. The button is thus outside the RatingControl's bounds (easily proved by giving the RatingControl a background color — the background color doesn't appear). The RatingControl does not clip to its bounds, so the button is still visible — but, being outside its superview, it is not tappable.
As for why the stack view is reducing the size of the RatingControl to zero: it's because the RatingControl has no intrisicContentSize. The reason why we need this is complicated and has to do with how a stack view manipulates its arranged subviews and gives them constraints. But in essence, it makes a lot of its decisions based on an arranged view's intrinsicContentSize.
In the comments you said:
In size inspector it has intrinsic size set to placeholder, width 240, height 44. Or is there something I need to do to stop the stack view making it zero sized?
The key thing to understand here is that that setting in the size inspector is only a placeholder. It is merely to shut the storyboard up so that it won't complain. It doesn't actually give the view any intrinsic size at runtime. To do that, you must actually override the intrinsicContentSize property, in your code, as a computed variable returning an actual size.
Looks like you're stumbling through the "Start Developing iOS Apps (Swift)" tutorial before it's updated for iOS 10 and Swift 3. Amazing you've made it this far! Anyhow, #matt has a great technical explanation below but did not include the actual code to get your tutorial working:
Add the following to your class definition (outside the init function):
override var intrinsicContentSize: CGSize {
return CGSize(width: 240, height: 44)
}
then your code will work : -
class RatingView: UIView {
// MARK: Initialisation
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Create a square button
let button = UIButton(frame: CGRect(x: 200, y: 100, width: 44, height: 44))
// Set the background colour of the button
button.backgroundColor = UIColor.darkGray
button.addTarget(self, action: #selector(self.ratingButtonTapped(_:)), for: .touchDown)
// Add the button to the view
addSubview(button)
}
// MARK: Button Action
func ratingButtonTapped(_: UIButton) {
print("Button pressed")
}
}
I'm having trouble mixing story boards and coded autolayout in Cocoa + Swift. It should be possible right?
I started with a NSTabViewController defined in a story board with default settings as dragged out of the toolbox. I added an NSTextField view via code. And I added anchors. Everything works as expected except the bottom anchor.
After adding the bottom anchor, the window and controller seem to collapse to the size of the NSTextField. I expected the opposite, that the text field get stretched to fill the height of the window.
What am I doing wrong? The literal Frame maybe? Or some option flag that I'm not setting?
class NSTabViewController : WSTabViewController {
var summaryView : NSTextField
required init?(coder: NSCoder) {
summaryView = NSTextField(frame: NSMakeRect(20,20,200,40))
summaryView.font = NSFont(name: "Menlo", size: 9)
super.init(coder: coder)
}
override func viewDidLoad() {
self.view.addSubview(summaryView)
summaryView.translatesAutoresizingMaskIntoConstraints = false
summaryView.topAnchor.constraintEqualToAnchor(self.view.topAnchor, constant: 5).active = true
summaryView.leftAnchor.constraintEqualToAnchor(self.view.leftAnchor, constant: 5).active = true
summaryView.rightAnchor.constraintEqualToAnchor(self.view.rightAnchor, constant: -5).active = true
summaryView.bottomAnchor.constraintEqualToAnchor(self.view.bottomAnchor, constant: -5).active = true
}
To prevent window from collapsing set lower priority for hugging:
summaryView.setContentHuggingPriority(249, forOrientation: .Vertical)
But you actually misuse tab view controller. It just manages views in common use... while you are adding text to the tab header area. There is a very good tutorial of how to use it correctly: https://www.youtube.com/watch?v=TS4H3WvIwpY