Selectors in custom UIViewController not calling functions within class - ios

I'm working on a simple game in my free time which has two view controllers: the default ViewController, and GameViewController. I have it setup so that GameViewController is a subview of a UIView within my ViewController, which I did so that I could customize transitions between my main menu and the actual game (I would love to find some more efficient ways of doing this in the future!)
The problem I'm currently having, is that selectors which I declare in my GameViewController class won't call functions within the GameViewController class, but, selectors do call functions within my ViewController class just fine. For example, here's some code in my ViewController class and GameViewController classes:
class ViewController: UIViewController {
#objc func myFunc() {
print("Some output to show that the ViewController function is working")
}
}
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let myButton = UIButton()
myButton.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
myButton.backgroundColor = .green
// This works fine. The function in ViewController is called, and I get some output in the console
myButton.addTarget(self, action: #selector(ViewController.myFunc), for:.touchUpInside)
// This does NOT work. No output is shown, therefore the function in GameViewController never got called
myButton.addTarget(self, action: #selector(self.myFunc2), for:.touchUpInside)
view.addSubview(myButton)
}
#objc func myFunc2() {
print("Some different output to show that the GameViewController function is working")
}
}
I also tried, just in case there was some problem with my button, to use Notifications to call my function. Since I knew my function in ViewController was working, I posted a notification from that function and added an observer to my GameViewController:
class ViewController: UIViewController {
#objc func myFunc() {
print("Some output to show that the ViewController function is working")
NotificationCenter.default.post(name: .myNotification, object: nil)
}
}
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let myButton = UIButton()
myButton.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
myButton.backgroundColor = .green
myButton.addTarget(self, action: #selector(ViewController.myFunc), for:.touchUpInside)
view.addSubview(myButton)
NotificationCenter.default.addObserver(self, selector: #selector(self.myFunc2(notification:)), name: .myNotification, object: nil)
}
#objc func myFunc2(notification: NSNotification) {
print("Some different output to show that the GameViewController function is working")
}
}
extension Notification.Name {
static let myNotification = Notification.Name("myNotification")
}
And even using Notifications, the function was called in ViewController, the notification was (supposedly) posted, and still my GameViewController function never got called.
I've used breakpoints to make sure 100% that my observer is being called before the notification is posted, and I've called myFunc2 in GameViewController's viewDidLoad and got output, so I really can't understand why I'm having so much trouble.
Any insights help, thanks!

Well, I made it work.
It turns out that, and I'm honestly not 100% sure this is totally accurate, because I was using a convenience init (something I forgot to mention in the OP), "self" ended up pointing to the current instance of ViewController and so when I tried to call "self.myFunc2," it was actually trying to call "ViewController.myFunc2" which obviously didn't exist.
It doesn't make sense to my why it wasn't outright crashing when it tried to call a nonexistent function, and I don't really get why "self" wasn't working as intended, but I guess that's a case closed.

Related

How do we prevent calling the method programmatically?

We need to prevent a method call programmatically. We just want to call method with user interaction from UI.
Actually, We're developing a SDK. We have some custom UI object classes. We want to avoid the user access to target methods without using our custom UI objects.
UIButton is just an example. It can be UISwitch or another UI element. Or maybe SwiftUI elements.
This is required as a security measure. It is a precaution we want to put so that malicious people do not call it as if it is an operation from the interface.
We want the operation to be performed only from the interface. So we check the information in Thread.callStackSymbols. But this code doesn't work in Testflight or release. It only works in debug mode.
You will see a UIButton below example. When clicked it’ll call clickedButton. But there is a method that call maliciousMethod. It can call clickedButton programmatically. We want to prevent it.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(frame: CGRect(x: 100, y: 100, width: 200, height: 50))
button.backgroundColor = .red
button.setTitle("Click Me", for: .normal)
self.view.addSubview(button)
button.addTarget(self, action: #selector(buttonClicked(_:)), for: .touchUpInside)
}
#objc func buttonClicked(_ sender : UIButton) {
/// We need to check action called from UI or another method here.
let symbols = Thread.callStackSymbols
let str: String = symbols[3]
if str.contains("sendAction") == false && str.contains("SwiftUI7Binding") == false {
print("It's called from programmatically. Abort")
return
}
}
/// We want to prevent this kind of call
func maliciousMethod() {
buttonClicked(UIButton())
}
}
There's not much you can do if someone has access to your code base, so I assume your ViewController is part of a binary framework to be distributed, that you want to secure against malicious programmers. In that case, you could store the button you want to allow as a private or fileprivate property in your ViewController. Then check for it in buttonClicked().
So something like this:
class ViewController: UIViewController {
fileprivate var secureButton: UIButton! // <-- Added this
override func viewDidLoad() {
super.viewDidLoad()
// Using/saving the secureButton here
secureButton = UIButton(frame: CGRect(x: 100, y: 100, width: 200, height: 50))
...
}
#objc func buttonClicked(_ sender : UIButton) {
/*
Check for the expected sender here. You probably don't want to
actually fatalError, but rather do something more sensible for
you app/framework
*/
guard sender === secureButton else {
fatalError("Strange things are afoot at the Circle-K")
}
...
}
/// This method will now trigger the guard in buttonClicked
func maliciousMethod() {
buttonClicked(UIButton())
}
}

Either wait X seconds or for a user input

I'm very new to swift so even this is a bit complicated to me at this point; sorry for sounding dumb if I do
Preface:
I have a main view controller(lets call it viewA) and a UIView which gets its functionality from a .xib file, let's call this viewB
This is all in the single main view controller page
The problem:
So i want my ViewController to execute a bunch of methods in sequence one of which is to call the result from a function in this viewB(its a subview so i cant use segues)
So in the function i want to return the result only when either--
A button to be pressed
30 seconds have passed
Whats the most efficient way to tackle this problem?
EDIT:
In a nutshell i want to make my main queue execution wait till there is an input from the player or 30 seconds have passed
Code structure:
ViewController:
class ViewController{
var viewB:CustomView
//methods
function to execute{
viewB.executeFunction()
}
}
CustomView:
class CustomView:UIView{
//functions of initializing buttons and text boxes
func executeFunction(){
//wait for a user input to complete then return from this function. i cant figure out how this works
}
}
Image of the UI idea
I think Option 1 is more suitable for your scenario. You can call the block when the button in your nib is pressed. Following is the code how you can use block in swift:
// Code for your UIViewController
func presentPopup() {
let popup = NibClassName.instantiateViewFromNib()
popup.btnPressBlock = { [unowned self] (success) -> () in
// Code after button of UIView pressed
}
popup.frame = CGRect(x: 100, y: 100, width: 300, height: 300)
self.view.addSubview(popup)
}
// Code of your UIView class file
var btnPressBlock: ((Bool) -> ())?
class func instantiateViewFromNib() -> NibClassName {
let obj = Bundle.main.loadNibNamed("NibName", owner: nil, options: nil)![0] as! NibClassName
return obj
}
#IBAction func btnPressed(sender: UIButton) {
btnPressBlock!(true)
removeFromSuperview()
}

Re-use animation throughout iOS views

Not sure if my thinking here is correct but I have similar animations I use throughout my iOS project and I would like to condense it to 1 file and reuse wherever I want.
A brief example. In my animations file I have a scale animation
Animations.swift
class Animations {
class func scaleSmall(_ view: UIView) {
let scaleAnim = POPBasicAnimation(propertyNamed: kPOPLayerScaleXY)
scaleAnim?.toValue = NSValue(cgSize: CGSize(width: 0.9, height: 0.9))
view.layer.pop_add(scaleAnim, forKey: "scaleSmallAnim")
}
}
Here I have one of my many swift files in my View folder and I would like to add that animation to the button
Button.swift
class Button: UIButton {
override func awakeFromNib() {
super.awakeFromNib()
self.addTarget(self, action: #selector(Animations.scaleSmall(_:)), for: .touchDown)
}
}
I thought I would be able to reference the animation from an additional file however everytime I do it this way I get the same error
Argument of '#selector' refers to instance method 'scaleSmall' that is not exposed to Objective-C
Am I referencing this function wrong?
try changing class func scaleSmall(_ view: UIView) {
to
#objc class func scaleSmall(view: UIView) {
I've confirmed my comment, so I'm posting an answer. Methods for UIButton need to be bridged to Obj-C. That's what #Kostas Tsoleridis suggests with his answer as well - it is not mixing two languages in one file, you are just marking the method for the compiler. Other solution would be to inherit from NSObject by your Animations class.
Now, as your confusion mentioned in a comment - it worked, because your Button class inherits from UIButton which is both from Obj-C world, and also inherits from NSObject down the chain.
To also address the issue mentioned in a comment under #Kostas Tsoleridis answer (and to be honest I should have thought about it before) - you can't pass self as a target and use a method from another class (even a static one). To solve this, you can use a singleton instance of your Animations class, something like this :
class Animations {
static let sharedInstance = Animations()
#objc class func scaleSmall(_ view: UIView) {
// your code
}
}
let button = UIButton()
button.addTarget(Animations.sharedInstance, action: #selector(Animations.scaleSmall(_:)), for: .touchDown)

How to write selector that takes an argument in Swift 3 [duplicate]

This question already has answers here:
"classname has no member functionname" when adding UIButton target
(4 answers)
Attach parameter to button.addTarget action in Swift
(14 answers)
Closed 6 years ago.
In Swift 2 this used to work (I have left out table view methods intentionally)...
import Foundation
import UIKit
private extension Selector {
static let didTapButton = #selector(TableVC.buttonTapped(_ :))
}
class TableVC: UITableViewController{
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let btnAccView:UIButton = UIButton(frame: CGRectMake(0, 0, 27, 27))
btnAccView.addTarget(self, action: .didTapButton, forControlEvents: UIControlEvents.TouchUpInside)
btnAccView.tag = indexPath.row
}
func buttonTapped(sender: UIButton){
print("button tapped at row \(sender.tag)")
}
}
In Swift 3 this causes the error : "TableVC has no member buttonTapped".
I just need to write a selector that takes an argument.
I have tried fiddling with the syntax in every way I can think of but nothing has worked. Thanks for your help
First of all, you need to modify buttonTapped so that the first argument label is suppressed. Insert an underscore before the internal argument label so that it looks like this:
func buttonTapped(_ sender: UIButton) {
Before, its method signature was TableVC.buttonTapped(sender:), and now its signature will be TableVC.buttonTapped(_:). You might have to remove the space between the underscore and the colon in your extension, but it should work after that.
As a side note, is there any particular reason you're using an extension on Selector here? That extension model is most useful typically with stuff like notification names that can be easily misspelled, resulting in tricky bugs. Since the Swift compiler automatically validates #selector declarations no matter where they're placed, you can just put it right into your code—no extension necessary.
You can save yourself some grief by letting the compiler infer some of the function signature. In your case...
class TableVC: UIViewController {
func buttonTapped(sender: UIButton) { /*...*/ }
}
You can refer to it from outside the TableVC class as either:
#selector(TableVC.buttonTapped(sender:))
#selector(TableVC.buttonTapped)
The second works because there's only one buttonTapped function on TableVC, so the compiler doesn't need the full signature to disambiguate it.
From code inside of TableVC you can simplify it a step further and just use #selector(buttonTapped), because at that point you're in the class scope and it can safely infer a method of the class.
(The #selector(TableVC.buttonTapped(_:)) line you're using works only if your method is declared as func buttonTapped(_ sender: UIButton). For your use case, it doesn't matter whether the sender parameter has an argument label or not — you just have to be consistent about it between your func declaration and your #selector expression.)
And as #Bob notes, there's no need for the Selector extension, because the compiler already validates the selector.
Use following way to implement selector for button action:
import UIKit
class MyViewController: UIViewController {
let myButton = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 50))
override init?(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
let action = #selector(MyViewController.tappedButton)
myButton.addTarget(self, action: action, forControlEvents: .touchUpInside)
}
func tappedButton(sender: UIButton?) {
print("tapped button")
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}

Click button in a UIViewController that was loaded a subView (UIView)

I'm trying to add a UIView subview into a UIViewController, and that UIView has a UISwitch that I want the user to be able to toggle. Based on the state, a UITextField's value will toggle back and forth. Here is the subview (InitialView):
import UIKit
class InitialView: UIView {
// All UI elements.
var yourZipCodeSwitch: UISwitch = UISwitch(frame: CGRectMake(UIScreen.mainScreen().bounds.width/2 + 90, UIScreen.mainScreen().bounds.height/2-115, 0, 0))
override func didMoveToSuperview() {
self.backgroundColor = UIColor.whiteColor()
yourZipCodeSwitch.setOn(true, animated: true)
yourZipCodeSwitch.addTarget(ViewController(), action: "yourZipCodeSwitchPressed:", forControlEvents: UIControlEvents.TouchUpInside)
self.addSubview(yourZipCodeSwitch)
}
}
If I want to have it's target properly pointing at the below function, where should I either set the target or include this function? I tried:
Setting the target in the UIViewController instead of the UIView
Keeping the function in the UIView
Here's the function:
// Enable/disable "Current Location" feature for Your Location.
func yourZipCodeSwitchPressed(sender: AnyObject) {
if yourZipCodeSwitch.on
{
yourTemp = yourZipCode.text
yourZipCode.text = "Current Location"
yourZipCode.enabled = false
}
else
{
yourZipCode.text = yourTemp
yourZipCode.enabled = true
}
}
And here is where I'm loading it into the UIViewController:
// add initial view
var initView : InitialView = InitialView()
// Execute on view load
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
view.addSubview(initView)
}
Any help is much appreciated - thanks!
Yeah, the didMoveToSuperView() placement doesn't make much sense. So you're creating a random, totally unconnected ViewController instance to make the compiler happy but your project sad. Control code goes in controllers, view code goes in views.
You need in your real ViewController:
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(initView)
// Note 'self' is the UIViewController here, so we got the scoping right
initView.yourZipCodeSwitch.addTarget(self, action: "yourZipCodeSwitchPressed:", forControlEvents: .ValueChanged)
}
Also, .TouchUpInside is for UIButtons. Toggle switches are much more complicated, so their events are different. Touching up inside on a toggle switch's current setting can and should do nothing, whereas touchup inside on the opposite setting triggers the control event above. iOS does all the internal hit detection for you.

Resources