Nested UIViews do not receive touch events - ios

I have a custom UIView that can be simplified down to:
class Node: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
let tapGR = UITapGestureRecognizer(target: self, action: "createChildNode")
addGestureRecognizer(tapGR)
userInteractionsEnabled = true
}
/* ... */
func createChildNode() {
let childNode = Node(frame: self.bounds.offset(dx: 100, dy: 100))
self.addSubview(childNode)
}
}
The first (root) node is created in the view controller:
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(Node(x: 100, y: 100, width: 150, height: 40))
}
For the root node, everything works as expected. However, for the children of the root node, nothing works.
To make sure that I wasn't using the UITapGestureRecognizer incorrectly, I tried to override the raw touchesBegan function for the Node class and do a simple println("tapped"). However the problem persisted and seems to be that the subviews do not receive any touches at all.
What could be causing this?

Sounds like you just need to pass the touch events through the view hierarchy. This answer should help:
https://stackoverflow.com/a/21436355/3259329

Related

Passing UIView control events through UIView extension

This might be a weird question, but i'm trying to code like a pro which obviously i am not.
Right now i have an extension which uses UIView and my concept is making it like an alert
For example, i coded the following:
extension UIView {
typealias completionHandler = (_ success:Bool) -> Void
private var screenWidth: CGFloat {
return UIScreen.main.bounds.width
}
public func showLuna(title messageTitle:String, message messageDescription:String, dissmiss dissmissDuration: TimeInterval) {
let luna = UIView()
luna.frame = CGRect(x: 16, y: 30, width: screenWidth - 30, height: 60)
luna.center.x = self.center.x
luna.backgroundColor = .white
luna.addShadow(radius: 11, opacity: 0.2)
luna.layer.cornerRadius = 10
}
}
And on my other ViewController, I use this to present Luna
#IBAction func presentLuna(_ sender: Any) {
self.view.showLuna(title: "Oooh", message: "Oops, something went horribly wrong!", dissmiss: 2.5);
}
At this very specific moment, I've been digging StackOverFlow for a day to find an answer. How do i attach a gesture recognizer or a function WITH a code block so the user can perform another task when luna gets tapped, or is that even possible with Extensions??
This is maybe what #rmaddy means by subclassing UIView:
First, create a subview so you can use it to respond to touch events:
class LunaView: UIView {
typealias LunaViewCompletionBlock = (_ isSuccessful: Bool) -> Void
var label: UILabel
//other things you need
var completionHandler: LunaViewCompletionBlock?
init(frame: CGRect, /* other properties such as title and colour */, completionHandler: LunaViewCompletionBlock?) {
self.completionHandler = completionHandler
self.label = UILabel()
super.init(frame: frame)
isUserInteractionEnabled = true
//this is how we handle touch
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTouch)))
addSubview(label)
// set up frame or constraints(if you use autolayout) for label and other views
// configure label.text and other things
}
func handleTouch() {
completionHandler(/*true or false*/)
}
/*other methods needed in this class*/
}
Then, to use it, simply do this in the extension:
extension UIView {
private var screenWidth: CGFloat {
return UIScreen.main.bounds.width
}
public func showLuna(title messageTitle:String, message messageDescription:String, dissmiss dissmissDuration: TimeInterval, completionHandler: LunaView.LunaViewCompletionBlock) {
let luna = LunaView(frame: /*size*/, /*other things like title*/, completionHandler: completionHandler)
// set luna's center, shadow, auto-dismiss time, colour.....
}
}
Answering your question:
How do i attach a gesture recognizer or a function WITH a code block so the user can perform another task when luna gets tapped, or is that even possible with Extensions??
You can't detect a touch event on just a general UIView you need a UIControl. Since the UIControl type inherits from UIView you will be able to do any of your typical drawing or view hierarchy stuff but you can also setup touch actions and callbacks using addTarget(:action:for:) or other UIControl mechanisms.

removeFromSuperview doesn't work with UIImageView

I add an image to my view by the following code if the count is zero and remove it otherwise:
var coverImageView = UIImageView()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if count == 0 {
let coverImage = UIImage(named: "AddFirstRecord")!
coverImageView = UIImageView(image: coverImage)
coverImageView.frame = CGRect(x: 20, y: 5, width: tableView.frame.width-20, height: 100)
view.addSubview(coverImageView)
} else {
DispatchQueue.main.async {
self.coverImageView.removeFromSuperview()
}
}
}
The problem is that it adds the image to the view, but removeFromSuperview does not work. (I made sure that it reaches to the else condition by debugging). I did the process in the main queue as well to be sure that the problem does not relate to threads. I wonder where is the origin of the issue?
In viewWillAppear the view still is not prepared completely to view. So removingFromSuperview does not have any effects. Instead, we should do the action inside viewDidLayoutSubviews:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if fetchedResultsController.fetchedObjects?.count == 0 {
let coverImage = UIImage(named: "AddFirstRecord")!
coverImageView.image = coverImage
coverImageView.frame = CGRect(x: 20, y: 5, width: tableView.frame.width-20, height: 100)
view.addSubview(coverImageView)
} else {
coverImageView.removeFromSuperview()
}
}
From Apple Documentation :
viewDidLayoutSubviews()
Called to notify the view controller that its
view has just laid out its subviews.
Your view controller can override this method to make changes after
the view lays out its subviews. The default implementation of this
method does nothing.

Why I init the uiview and the uiview also deinit by swift?

I want to manage memory so I want to deinit the UIview when leaving ViewController.
And I try to use keyword "weak" and I get crash because my chatkeyboard is nil.
I dont know why making it crash.
Thanks.
class ChatKeyboard: UIView {
var buttonMic:UIButton = { ()->UIButton in
let ui:UIButton = GeneratorButton()
return ui
}()
override init(frame: CGRect) {
super.init(frame: frame)
print("===) ChatKeyboard init.")
translatesAutoresizingMaskIntoConstraints = false
loadContent()
loadConstrain()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
deinit {
print("===) ChatKeyboard deinit.")
}
func loadContent() {
backgroundColor = UIColor.white
addSubview(buttonMic)
}
func loadConstrain() {
buttonMic.snp.makeConstraints { (make) -> Void in
make.left.equalTo(micLeftPadding)
make.top.equalTo(micTopPadding)
make.width.equalTo(UIScreen.main.bounds.width*0.0581)
make.height.equalTo(UIScreen.main.bounds.height*0.045)
}
}
}
class ChatroomViewController: UIViewController{
weak var chatKeyboard:ChatKeyboard?
override func viewDidLoad() {
super.viewDidLoad()
chatKeyboard = ChatKeyboard(frame: CGRect(x: 0, y: 0, width: 300, height: 44))
}
}
I set breakpoint at "chatKeyboard = ChatKeyboard(frame: CGRect(x: 0, y: 0, width: 300, height: 44))"and then my log print:
===) ChatKeyboard init.
===) ChatKeyboard deinit.
A weak variable will be destroyed as soon as there are no strong references to it.
If you're creating a view and assigning it directly to a weak variable, it will be destroyed immediately. Weak IBOutlets work because they are added to a superview (creating a strong reference) before they are assigned to the variable. You can achieve this by using a local variable before assigning to your property:
let keyboard = ChatKeyboard(...)
view.addSubview(keyboard)
chatKeyboard = keyboard
However, there is no harm in your view controller having strong references to views it cares about, as long as those views don't also have strong references back to the view controller. They'll get destroyed when the view controller is destroyed.
Reason is you have declared your chatKeyBoard as
weak var chatKeyboard:ChatKeyboard?
Which means your viewController will not hold any strong reference to the view you load. Hence reference count of the view will not vary.
In your viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
chatKeyboard = ChatKeyboard(frame: CGRect(x: 0, y: 0, width: 300, height: 44))
}
You instantiate the view but because view was weakly held as soon as the control goes out of the scope of viewDidLoad ARC released the view.
If you want your view to be accessible even after viewDidLoad called declare a strong a refrence
var chatKeyboard:ChatKeyboard?
Hope it helps
In a class or structure, do not use keyword "weak" to describe the property which you will init in the same class or structure.

UISlider addTarget and sendActions not working in UTs

When I run this code in a standard MVC project, all works fine: I can programmatically send actions to my UISlider.
override func viewDidLoad() {
super.viewDidLoad()
slider = UISlider(frame: CGRect(x: 100, y: 100, width: 200, height: 50))
slider.minimumValue = 1
slider.maximumValue = 100
slider.addTarget(self, action: #selector(sliderTouch), for: .touchUpInside)
slider.sendActions(for: .touchUpInside)
}
func sliderTouch(sender: UISlider) {
print("value: \(sender.value)")
}
Now for some reason, when I want to simulate Slider behaviors in my UTs, it's not working. here is the code:
func testSlider() {
let slider = UISlider(frame: CGRect(x: 100, y: 100, width: 200, height: 50))
slider.minimumValue = 1
slider.maximumValue = 100
slider.addTarget(self, action: #selector(sliderTouch), for: .touchUpInside)
slider.sendActions(for: .touchUpInside)
}
func sliderTouch(sender: UISlider) {
print("value: \(sender.value)")
}
sliderTouch() is never called in the UT case and from my understanding, sendActions is not asnyc and should directly call the action methods.
So why am I getting this behavior, and how can I solve this?
Edit: I also need it to work for other UIKit controls such as UISwitch, UISegmentedControl, UIDatePicker etc...
I had a problem similar to yours when testing classes contained in a Framework target.
Here is an excerpt from Apple's documentation:
When a control-specific event occurs, the control calls any associated action methods right away. Action methods are dispatched through the current UIApplication object, which finds an appropriate object to handle the message, following the responder chain if needed.
When testing a classes in a framework there is no UIApplication object and thus the events can not be dispatched.
You can add a Host Application in your test target settings. If you do not have a host application, you can just create an empty iOS app and use that.
Also see this post.
Update:
Here is how to set a Host Application:
If there is no Application in the dropdown, simply create a new Target and use the Application > Single View Application template.
You are done and your tests should now work. There are no additional steps.
For swift 3:
https://github.com/ReactiveCocoa/ReactiveCocoa/blob/ae6ebb7725b3d5d33db039d456797c220720cb99/ReactiveCocoaTests/UIKit/UIControl%2BEnableSendActionsForControlEvents.swift
For Swift 2
https://github.com/RACCommunity/Rex/blob/master/Tests/Helpers/UIControl%2BEnableSendActionsForControlEvents.swift
Or try subclassing:
class TouchSlider: UISlider {
var touchUpInsideHandler: ((TouchSlider) -> Void)?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
touchUpInsideHandler?(self)
guard let touch = touches.first,
let imageWidth = currentThumbImage?.size.width else {
super.touchesBegan(touches, with: event)
return
}
let location = touch.location(in: self)
let newValue: Float = minimumValue + (maximumValue - minimumValue) * Float((location.x - imageWidth / 2.0) / (bounds.width - imageWidth))
setValue(newValue, animated: true)
super.touchesBegan(touches, with: event)
}
}
Use touchUpInsideHandler for managing touches outside.

Updating UILabel text when variable changes

I've got a UIControl class and need to do some calculation based on UIImageView location which can be moved with touchesBegan and touchesMoved (everything inside this class).
Than I would like to display it as a UILabel which I've created programmatically.
class control : UIControl{
...
let leftControl: UIImageView = UIImageView(image: UIImage(named: "left-control"))
...
func leftValue() -> String{
var leftValue : String = "0.0"
leftValue = "\(leftControl.center.x)"
return leftValue
}
}
and my ViewController.swift
class ViewController: UIViewController {
let ctrl : Control = Control()
let leftLabel : UILabel = UILabel(frame: CGRect(x: 40, y: 300, width: 150, height: 30))
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
ctrl.frame.origin = CGPoint(x: 40, y: 400)
leftLabel.text = "\(ctrl.leftValue())" //displays only starting value
view.addSubview(slider)
view.addSubview(leftLabel)
view.addSubview(rightLabel)
}
I know that it's inside the viewDidLoad so it's not updating properly. I was wondering about scheduledTimer but don't know if it's good solution.
You can achieve this using protocols and delegation - in the file for your Control add this :
protocol ControlDelegate: class {
func controlPositionDidChange(leftValue: String)
}
And add a weak var delegate: ControlDelegate? inside Control class.
In the file for view controller make following changes :
class ViewController: UIViewController, ControllDelegate {
let ctrl : Control = Control()
let leftLabel : UILabel = UILabel(frame: CGRect(x: 40, y: 300, width: 150, height: 30))
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
ctrl.frame.origin = CGPoint(x: 40, y: 400)
ctrl.delegate = self
leftLabel.text = "\(ctrl.leftValue())" //displays only starting value
view.addSubview(slider)
view.addSubview(leftLabel)
view.addSubview(rightLabel)
}
func controlPositionDidChange(leftValue: String) {
leftLabel.text = leftValue
}
}
Now, whenever you want to inform the delegate that your control has changed the position, simply call self.delegate?.controlPositionDidChange(self.leftValue()) in appropriate places.
As usually, there is more in the docs. I highly suggest reading through them as delegation and protocols are widely used in CocoaTouch.
The answer of #Losiowaty describes the solution that most developers choose. But there are (in my opinion) much better ways to achieve it. I prefer the object oriented solution. It might look like more code, but its a much better maintainable way with more reusable code.
A real object oriented solution of your problem might look like that:
// reusable protocol set
protocol OOString: class {
var value: String { get set }
}
// reusable functional objects
final class RuntimeString: OOString {
init(initialValue: String = "") {
self.value = initialValue
}
var value: String
}
final class ViewUpdatingString: OOString {
init(_ decorated: OOString, view: UIView) {
self.decorated = decorated
self.view = view
}
var value: String {
get {
return decorated.value
}
set(newValue) {
decorated.value = newValue
view.setNeedsLayout()
}
}
private let decorated: OOString
private let view: UIView
}
// reusable ui objects
final class MyLabel : UILabel {
init(frame: CGRect, imageXCenter: OOString) {
self.imageXCenter = imageXCenter
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("Not supported")
}
override func layoutSubviews() {
super.layoutSubviews()
text = imageXCenter.value
}
private let imageXCenter: OOString
}
final class MyControl : UIControl {
init(frame: CGRect, imageXCenter: OOString) {
self.imageXCenter = imageXCenter
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("Not supported")
}
private let imageXCenter: OOString
private let leftControl = UIImageView(image: UIImage(named: "left-control"))
// call this at change
private func updateValue() {
imageXCenter.value = "\(leftControl.center.x)"
}
}
// non reusable business logic
final class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let dependency = RuntimeString(initialValue: "unset")
let leftLabel = MyLabel(
frame: CGRect(x: 40, y: 300, width: 150, height: 30),
imageXCenter: dependency
)
let control = MyControl(
frame: CGRect(x: 40, y: 400, width: 400, height: 400),
imageXCenter: ViewUpdatingString(dependency, view: leftLabel)
)
view.addSubview(leftLabel)
view.addSubview(control)
}
}
The main idea is to extract the dependency of both objects into another object and use a decorator then to automatically update the ui on every set value.
Note:
this approach follows the rules of object oriented coding (clean coding, elegant objects, decorator pattern, ...)
reusable classes are very simple constructed and fullfill exactly one task
classes communicate by protocols with each other
dependencies are given by dependency injection as far as possible
internal object functionality is private (loose coupling)
everything (except the business logic) is designed for reuse -> if you code like that the portfolio of reusable code grows with every day you code
the business logic of your app is more concentrated in one place (in my real coding its even outside the UIViewController)
unittesting of reusable objects is very simple when using fake implementations for the protocols (even mocking is not needed in most cases)
lesser problems with retain cycles, in most cases you do not need weak properties any more
avoiding Null, nil and Optionals (they pollute your code)
...

Resources