How to get action on button pressed in a component to viewcontroller - swift

I've made a UIView component, inside has 2 buttons. When a user touch the buttons, I need to call a action method of ViewController.
I've tried to pass a selector, but it crash:
on ViewController
component.leftAction = #selector(ViewController.myAction)
func myAction(sender: UIButton!) {
print("tapped!")
}
on Component.swift
public var leftAction: Selector? {
didSet {
if (leftAction != nil){
self.leftButton.addTarget(self, action: leftAction!, for: .touchUpInside)
}
}
}
How can I do it?

It's crashing because button target is wrong. In your case you are passing target as self which is an instance of Component class. Target must be your ViewController instance because your selector is defined in your ViewController class.
You can do something like this to fix your problem.
public var leftTargetAction: (Any, Selector)? {
didSet {
if let targetAction = leftTargetAction {
self.leftButton.addTarget(targetAction.0, action: targetAction.1, for: .touchUpInside)
}
}
}
And use it like this.
component.leftTargetAction = (self,#selector(ViewController.myAction))

Better approach would be to handle this with delegates.
To know more about delegate you can go through this post.

Just change
component.leftAction = #selector(ViewController.myAction)
to
component.leftAction = #selector(ViewController.myAction(sender:))
And ViewController should not be deallocated.

Related

Swift - How to get the sender tag for an array of buttons using UILongPressGestureRecognizer?

I have buttons in the storyboard that I put into a Referencing Outlet Collection. I'm using UITapGestureRecognizer and UILongPressGestureRecognizer for all of these buttons, but how can I print exactly which button gets tapped? Bellow is what I tried but doesn't work. I get an error that says "Value of type 'UILongPressGestureRecognizer' has no member 'tag'." I'm trying to build the button grid for the Minesweeper game. Thank you for your help.
class ViewController: UIViewController {
#IBOutlet var testButtons: [UIButton]! // There are 100 buttons in this array
override func viewDidLoad() {
super.viewDidLoad()
let testButtonPressed = UILongPressGestureRecognizer(target: self, action: #selector(testPressed))
testButtonPressed.minimumPressDuration = 0.5
// These indexes are just to test how to recognize which button gets pressed
testButtons[0].addGestureRecognizer(testButtonPressed)
testButtons[1].addGestureRecognizer(testButtonPressed)
}
#objc func testPressed(_ sender: UILongPressGestureRecognizer) {
print("Test button was pressed")
print(sender.tag) // THIS DOESN'T WORK, BUT CONCEPTUALLY THIS IS WHAT I WANT TO DO
}
This error occurs because UILongPressGestureRecognizer object has no tag property
You can access sender's button in a way like that:
#objc func testPressed(_ sender: UILongPressGestureRecognizer) {
guard let button = sender.view as? UIButton else { return }
print(button.tag)
}
I think that the best solution to handle button's actions is to add #IBAction
(you can add it like #IBOutlet with a minor change - set Action connection type)
And then in #IBAction block you cann access all button properties (like tag and others)
instead of using gesture I think it would be better to use #IBAction and connect the buttons With it here is a small example
UILongPressGestureRecognizer which is a subclass of UIGestureRecognizer, can be used only once per button or view. Because UILongPressGestureRecognizer has only a single view property. In your code, it will always be testButtons[1] calling the testPressed action. So you have to first modify the viewDidLoad code like this :-
for button in testButtons {
let testButtonPressed = UILongPressGestureRecognizer(target: self, action: #selector(testPressed))
testButtonPressed.minimumPressDuration = 0.5
button.addGestureRecognizer(testButtonPressed)
button.addGestureRecognizer(testButtonPressed)
}
Then you can access the button from testPressed like this (I hope you've already set the tag in the storyboard) :-
#objc func testPressed(_ sender: UILongPressGestureRecognizer) {
if sender.state == .began {
if let button = sender.view as? UIButton {
print(button.tag)
}
}
}
You need to set tags before pressing!
On the viewDidLoad() method you must add something like:
testButtons.enumerated().forEach {
let testButtonPressed = UILongPressGestureRecognizer(target: self, action: #selector(testPressed))
testButtonPressed.minimumPressDuration = 0.5
$0.element.addGestureRecognizer(testButtonPressed)
$0.element.tag = $0.offset
}
And when the long press is receiving you need to get a tag from view not from the sender!
print(sender.view?.tag)
Since a gesture recognizer should only be associated with a single view, and doesn't directly support using an identity tag to match it with buttons. When creating an array of buttons for a keyboard, with a single gesture response function, I found it easier to use the gesture recognizer "name" property to identify the associated button.
var allNames: [String] = []
// MARK: Long Press Gesture
func addButtonGestureRecognizer(button: UIButton, name: String) {
let longPrssRcngr = UILongPressGestureRecognizer.init(target: self, action: #selector(longPressOfButton(gestureRecognizer:)))
longPrssRcngr.minimumPressDuration = 0.5
longPrssRcngr.numberOfTouchesRequired = 1
longPrssRcngr.allowableMovement = 10.0
longPrssRcngr.name = name
allNames.append(name)
button.addGestureRecognizer(longPrssRcngr)
}
// MARK: Long Key Press
#objc func longPressOfButton(gestureRecognizer: UILongPressGestureRecognizer) {
print("\nLong Press Button => \(String(describing: gestureRecognizer.name)) : State = \(gestureRecognizer.state)\n")
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
if let keyName = gestureRecognizer.name {
if allNames.contains(keyName) {
insertKeyText(key: keyName)
} else {
print("No action available for key")
}
}
}
}
To implement, call the addButtonGestureRecognizer function after creating the button, and provide a name for the button (I used the button text) e.g.
addButtonGestureRecognizer(button: keyButton, name: buttonText)
The button name is stored in the "allNames" string array so that it can be matched later in "longPressOfButton".
When the button name is matched in the "longPressOfButton" response function, it passes it to "addKeyFunction" where it is processed.

Should the button target method logic be defined in UIView or UIViewController

I have a button and a textview defined in a UIView Class. Which will make a networks request when pressed. Should i add the button.addTarget in UIView or UIViewController. what is the MVC way to do it.
class MessageInputView: UIView {
let send_button: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .red
button.setTitle("Send", for: .normal)
button.setTitleColor(UIColor.black, for: .normal)
return button
}()
let textView: UITextView = {
let textView = UITextView()
textView.translatesAutoresizingMaskIntoConstraints=false
textView.clipsToBounds = true
textView.layer.cornerRadius = 19.5
textView.layer.borderWidth = 1
textView.layer.borderColor = UIColor.inputTextViewColor.cgColor
return textView
}()
}
class ChatMessagesViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
let messageInputView = MessageInputView()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
messageInputView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(messageInputView)
setMessagesInputViewConstraints()
}
}
If you have custom class for your view element, you should declare IBAction in that class, but the logic should be happening in the view controller (or other responsible class in other architectures).
You can establish connection between view and view controller via delegate pattern or with the help of closures, whichever fits your code better.
Here's the example of the closure:
class CustomView: UIView {
// ...
var buttonHandler: (() -> Void)?
#IBAction func buttonAction(_ sender: UIButton) {
buttonHandler?()
}
// ...
}
class ViewController: UIViewController {
// ...
override func viewDidLoad() {
// ...
customView.buttonHander = { print("User clicked the button") }
}
}
Let's look at the difference between the model, view, and controller.
The view is how the user interacts with the application. It should only be concerned with getting input data from the user and sending output data to the user. Any other operations should be delegated to something that is not the view.
The model is how the application handles its required operations. It should not know or care about how/where the result of those operations is being used. It simply performs a requested action and returns the result to the requester.
The controller is what sets up the communication pathways/adapters between the view and the model. It is the only entity that knows both the view and the model exist; they should not know about the existence of each other or the controller, since they don't have to care about it. All they need is an adapter that allows them to communicate with some external entity, which the controller provides.
Given this information, it makes sense to me that your button, when clicked, should make a request to the thing(s) that handle its requests through its adapter. That adapter routes the request to the model, which performs the operation and returns the result. Thus, the view knows nothing about where its request is going or where the response is coming from, and the model has no idea that the view is the one requesting the action and response. This preserves encapsulation.

Keyboard overlaying action sheet in iOS 13.1 on CNContactViewController

This seems to be specific to iOS 13.1, as it works as expected on iOS 13.0 and earlier versions to add a contact in CNContactViewController, if I 'Cancel', the action sheet is overlapping by keyboard. No actions getting performed and keyboard is not dismissing.
Kudos to #GxocT for the the great workaround! Helped my users immensely.
But I wanted to share my code based on #GxocT solution hoping it will help others in this scenario.
I needed my CNContactViewControllerDelegate contactViewController(_:didCompleteWith:) to be called on cancel (as well as done).
Also my code was not in a UIViewController so there is no self.navigationController
I also dont like using force unwraps when I can help it. I have been bitten in the past so I chained if lets in the setup
Here's what I did:
Extend CNContactViewController and place the swizzle function in
there.
In my case in the swizzle function just call the
CNContactViewControllerDelegate delegate
contactViewController(_:didCompleteWith:) with self and
self.contact object from the contact controller
In the setup code, make sure the swizzleMethod call to
class_getInstanceMethod specifies the CNContactViewController
class instead of self
And the Swift code:
class MyClass: CNContactViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.changeImplementation()
}
func changeCancelImplementation() {
let originalSelector = Selector(("editCancel:"))
let swizzledSelector = #selector(CNContactViewController.cancelHack)
if let originalMethod = class_getInstanceMethod(object_getClass(CNContactViewController()), originalSelector),
let swizzledMethod = class_getInstanceMethod(object_getClass(CNContactViewController()), swizzledSelector) {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
// dismiss the contacts controller as usual
viewController.dismiss(animated: true, completion: nil)
// do other stuff when your contact is canceled or saved
...
}
}
extension CNContactViewController {
#objc func cancelHack() {
self.delegate?.contactViewController?(self, didCompleteWith: self.contact)
}
}
The keyboard still shows momentarily but drops just after the Contacts controller dismisses.
Lets hope apple fixes this
I couldn't find a way to dismiss keyboard. But at least you can pop ViewController using my method.
Don't know why but it's impossible to dismiss keyboard in CNContactViewController. I tried endEditing:, make new UITextField firstResponder and so on. Nothing worked.
I tried to alter action for "Cancel" button. You can find this button in NavigationController stack, But it's action is changed every time you type something.
Finally I used method swizzling. I couldn't find a way to dismiss keyboard as I mentioned earlier, but at least you can dismiss CNContactViewController when "Cancel" button is pressed.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
changeImplementation()
}
#IBAction func userPressedButton(_ sender: Any) {
let controller = CNContactViewController(forNewContact: nil)
controller.delegate = self
navigationController?.pushViewController(controller, animated: true)
}
#objc func popController() {
self.navigationController?.popViewController(animated: true)
}
func changeImplementation() {
let originalSelector = Selector("editCancel:")
let swizzledSelector = #selector(self.popController)
if let originalMethod = class_getInstanceMethod(object_getClass(CNContactViewController()), originalSelector),
let swizzledMethod = class_getInstanceMethod(object_getClass(CNContactViewController()), swizzledSelector) {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
}
PS: You can find additional info on reddit topic: https://www.reddit.com/r/swift/comments/dc9n3a/bug_with_cnviewcontroller_ios_131/
Fixed in iOS 13.4
Tested in Xcode Simulator
NOTE: This bug is now fixed. This question and answer were applicable only to some particular versions of iOS (a limited range of iOS 13 versions).
The user can in fact swipe down to dismiss the keyboard and then tap Cancel and see the action sheet. So this issue is regrettable and definitely a bug (and I have filed a bug report) but not fatal (though, to be sure, the workaround is not trivial for the user to discover).
Thanks, #GxocT for your workaround, however, the solution posted here is different from the one you posted on Reddit.
The one on Reddit works for me, this one doesn't so I want to repost it here.
The difference is on the line with swizzledMethod which should be:
let swizzledMethod = class_getInstanceMethod(object_getClass(self), swizzledSelector) {
The whole updated code is:
class MyClass: CNContactViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.changeImplementation()
}
func changeCancelImplementation() {
let originalSelector = Selector(("editCancel:"))
let swizzledSelector = #selector(CNContactViewController.cancelHack)
if let originalMethod = class_getInstanceMethod(object_getClass(CNContactViewController()), originalSelector),
let swizzledMethod = class_getInstanceMethod(object_getClass(self), swizzledSelector) {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
// dismiss the contacts controller as usual
viewController.dismiss(animated: true, completion: nil)
// do other stuff when your contact is canceled or saved
...
}
}
extension CNContactViewController {
#objc func cancelHack() {
self.delegate?.contactViewController?(self, didCompleteWith: self.contact)
}
}
Thanks #Gxoct for his excellent work around. I think this is very useful question & post for those who are working with CNContactViewController. I also had this problem (till now) but in objective c. I interpret the above Swift code into objective c.
- (void)viewDidLoad {
[super viewDidLoad];
Class class = [CNContactViewController class];
SEL originalSelector = #selector(editCancel:);
SEL swizzledSelector = #selector(dismiss); // we will gonna access this method & redirect the delegate via this method
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
Creating a CNContactViewController category for accessing dismiss;
#implementation CNContactViewController (Test)
- (void) dismiss{
[self.delegate contactViewController:self didCompleteWithContact:self.contact];
}
#end
Guys who are not so familiar with Swizzling you may try this post by matt
One thing to always take into account is that swizzler method is executed only once. Make sure that you implement changeCancelImplementation() in dispatch_once queue so that it is executed only once.
Check this link for description
Also this bug is found only in iOS 13.1, 13.2 and 13.3

Swift: How to pass a Parameter to a function which itself is also a Parameter?

It's like this:
var i = 0
button.addTarget(self, action:"showContent", forControlEvents: UIControlEvents.TouchUpInside)
and the function "showContent" is like this:
func showContent(i: Int) {
//do sth.
}
I want to pass the variable i to function showContent when that button be touched, how could i do ?
In the target-action pattern, you can't do that. You can either use "showContent", in which case your function will be:
func showContent() {
}
… or you can add a colon ("showContent:") in which case your function will be:
func showContent(sender : UIButton) {
// do something with sender
}
This helps enforce the broader Model / View / Controller pattern by making it difficult for your views to be "smart". Instead, your model and controller objects should handle i. A button simply displays what it's told, and tells your controller when it's tapped.
You can read i, and respond to it, in the appropriate method. For example:
class ViewController : UIViewController {
var i = 0
var button = UIButton(type: .System)
var label = UILabel()
override func viewDidLoad() {
button.addTarget(self, action:"showContent", forControlEvents: UIControlEvents.TouchUpInside)
}
func showContent() {
i++
label.text = "Current value of i: \(i)"
}
}

Call button clicked method in parent class in swift

I have a ViewController that calls a class to build a menu. This menu draw a button with a buttonClicked method. I call this menu from many different ViewControllers so I need this menu to call a different button method depending on the ViewController it was called from. I cannot think how to program this?
class MenuController : UIViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
var menu = Menu()
self.view.addSubview(menu)
}
func buttonClicked(sender:UIButton)
{
var tag = sender.tag
println("I want the button click method to call this method")
}
}
class Menu:UIView
{
init()
{
var button:UIButton = UIButton.buttonWithType(UIButtonType.Custom) as UIButton
button.frame = CGRectMake(0,0,280, 25)
button.addTarget(self, action: "buttonClicked:", forControlEvents: UIControlEvents.TouchUpInside)
button.tag = Int(itemNo)
menu.addSubview(button)
}
func buttonClicked(sender:UIButton)
{
var tag = sender.tag
println(tag)
}
}
This is a perfect use case for either a closure or a delegate/protocol:
Closure option
In your Menu class, create a public variable (say buttonCode) that will host your closure:
class Menu:UIView
{
var buttonCode : ()->()
and your buttonClicked function becomes:
func buttonClicked(sender:UIButton) {
self.buttonCode()
}
Then in the controller, you set up menu.buttonCode = { println("hello") }, and that's it.
Protocol option
You create a protocol for your Menu, that expects a buttonCode() function. You also create a var in the Menu class to host the weak reference for the delegate. Your view controller implements the protocol and the buttonCode() function. Then your buttonClicked function becomes:
func buttonClicked(sender:UIButton) {
self.delegate.buttonCode()
}
I personally prefer today to use the closure option, it's cleaner and simpler, at least in this situation. Please see http://www.reddit.com/r/swift/comments/2ces1q/closures_vs_delegates/ for a more in-depth discussion.