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())
}
}
Related
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.
My view (in the Main.storyboard) contains another UIView which takes only about 80% of the screen's height (set with constraints).
In this second view (I call it the viewContainer) I want to display different views, which works with
viewContainer.bringSubview(toFront: someView)
I created a new group in my project which contains 3 UIViewController classes and 3 xib files which I want to display in my viewContainer.
For each of those xib files I changed the background color to something unique so I can tell if it's working. And it does so far.
Now I tried adding a UIButton to the first UIViewController class and added an #IBAction for it. That just prints text to the console.
When I run the app I can switch between my 3 classes (3 different background colors) and I can also see and click the button I added to the first class when I select it.
But the code is never executed and my console is empty. Is that because my 3 other Views are not shown in my Main.storyboard?
SimpleVC1.swift
import UIKit
class SimpleVC1: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func onButtonPressed(_ sender: Any) {
print("test")
}
}
Try using a target instead (and programmatically create the button), it might be easier:
import UIKit
class SimpleVC1: UIViewController {
var button = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(button)
button.frame.size = CGSize(width: 100, height: 100)
button.center = CGPoint(x: 100, y: 100)
button.setTitle("Control 1", for: .normal)
button.addTarget(self, action: #selector(control1), for: .touchUpInside)
button.backgroundColor = UIColor.blue
button.setTitleColor(UIColor.white, for: .normal)
}
#objc func control1() {
//Add your code for when the button is pressed here
button.backgroundColor = UIColor.red
}
}
addTarget
So I know that you can use addTarget on a button
like so:
import UIKit
class ViewController: UIViewController {
func calledMethod(_ sender: UIButton!) {
print("Clicked")
}
override func viewDidLoad() {
super.viewDidLoad()
let btn = UIButton(frame: CGRect(x: 30, y: 30, width: 60, height: 30))
btn.backgroundColor = UIColor.darkGray
btn.addTarget(self, action: #selector(self.calledMethod(_:)), for: UIControlEvents.touchUpInside)
self.view.addSubview(btn)
}
}
This works absolutely fine but I was just wondering is there another way to detect when something is clicked and run some code when that happens.
You can add button in the storyboard and create an outlet for it's action.
#IBAction func buttonAction(_ sender: UIButton) {
//implement your code here
}
A button is a subclass of UIControl. A UIControl is designed to use target/action to specify code to run when the specified action occurs. If you don't want to use that mechanism, you have to create an equivalent.
You could attach a tap gesture recognizer to some other object and set it's userInteractionEnabled flag to true, but gesture recognizers ALSO use the target/action pattern.
My suggestion is to "stop worrying and learn to love the button." Just learn to use target/action.
Another possibility: Create a custom subclass of UIButton that sets itself up as it's own target at init time (specifically in init(coder:) and init(frame:), the 2 init methods you need to implement for view objects.) This button would include an array of closures that its action method would execute if the button was tapped. It would have methods to add or remove closures. You'd then put such a button in your view controller and call the method to add the closure you want.
You can also have a look at RxSwift/RxCocoa or similar. All the changes and actions are added automatically and you can decide if you want to observe them or not.
The code would look something like this:
let btn = UIButton(frame: CGRect(x: 30, y: 30, width: 60, height: 30))
btn.backgroundColor = UIColor.darkGray
btn
.rx
.tap
.subscribe(onNext: {
print("Clicked")
}).disposed(by: disposeBag)
view.addSubview(btn)
Goal
I want to make a custom keyboard that is only used within my app, not a system keyboard that needs to be installed.
What I have read and tried
Documentation
App Extension Programming Guide: Custom Keyboard
Custom Views for Data Input
The first article above states:
Make sure a custom, systemwide keyboard is indeed what you want to
develop. To provide a fully custom keyboard for just your app or to
supplement the system keyboard with custom keys in just your app, the
iOS SDK provides other, better options. Read about custom input views
and input accessory views in Custom Views for Data Input in Text
Programming Guide for iOS.
That is what led me to the second article above. However, that article did not have enough detail to get me started.
Tutorials
iOS 8: Creating a Custom Keyboard in Swift
How to make a custom keyboard in iOS 8 using Swift
Xcode 6 Tutorial: iOS 8.0 Simple Custom Keyboard in Swift
Creating a Custom Keyboard Using iOS 8 App Extension
I was able to get a working keyboard from the second tutorial in the list above. However, I couldn't find any tutorials that showed how to make an in app only keyboard as described in the Custom Views for Data Input documentation.
Stack Overflow
I also asked (and answered) these questions on my way to answering the current question.
How to input text using the buttons of an in-app custom keyboard
Delegates in Swift
Question
Does anyone have a minimal example (with even one button) of an in app custom keyboard? I am not looking for a whole tutorial, just a proof of concept that I can expand on myself.
This is a basic in-app keyboard. The same method could be used to make just about any keyboard layout. Here are the main things that need to be done:
Create the keyboard layout in an .xib file, whose owner is a .swift file that contains a UIView subclass.
Tell the UITextField to use the custom keyboard.
Use a delegate to communicate between the keyboard and the main view controller.
Create the .xib keyboard layout file
In Xcode go to File > New > File... > iOS > User Interface > View to create the .xib file.
I called mine Keyboard.xib
Add the buttons that you need.
Use auto layout constraints so that no matter what size the keyboard is, the buttons will resize accordingly.
Set the File's Owner (not the root view) to be the Keyboard.swift file. This is a common source of error. See the note at the end.
Create the .swift UIView subclass keyboard file
In Xcode go to File > New > File... > iOS > Source > Cocoa Touch Class to create the .swift file.
I called mine Keyboard.swift
Add the following code:
import UIKit
// The view controller will adopt this protocol (delegate)
// and thus must contain the keyWasTapped method
protocol KeyboardDelegate: class {
func keyWasTapped(character: String)
}
class Keyboard: UIView {
// This variable will be set as the view controller so that
// the keyboard can send messages to the view controller.
weak var delegate: KeyboardDelegate?
// MARK:- keyboard initialization
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initializeSubviews()
}
override init(frame: CGRect) {
super.init(frame: frame)
initializeSubviews()
}
func initializeSubviews() {
let xibFileName = "Keyboard" // xib extention not included
let view = Bundle.main.loadNibNamed(xibFileName, owner: self, options: nil)![0] as! UIView
self.addSubview(view)
view.frame = self.bounds
}
// MARK:- Button actions from .xib file
#IBAction func keyTapped(sender: UIButton) {
// When a button is tapped, send that information to the
// delegate (ie, the view controller)
self.delegate?.keyWasTapped(character: sender.titleLabel!.text!) // could alternatively send a tag value
}
}
Control drag from the buttons in the .xib file to the #IBAction method in the .swift file to hook them all up.
Note that the protocol and delegate code. See this answer for a simple explanation about how delegates work.
Set up the View Controller
Add a UITextField to your main storyboard and connect it to your view controller with an IBOutlet. Call it textField.
Use the following code for the View Controller:
import UIKit
class ViewController: UIViewController, KeyboardDelegate {
#IBOutlet weak var textField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
// initialize custom keyboard
let keyboardView = Keyboard(frame: CGRect(x: 0, y: 0, width: 0, height: 300))
keyboardView.delegate = self // the view controller will be notified by the keyboard whenever a key is tapped
// replace system keyboard with custom keyboard
textField.inputView = keyboardView
}
// required method for keyboard delegate protocol
func keyWasTapped(character: String) {
textField.insertText(character)
}
}
Note that the view controller adopts the KeyboardDelegate protocol that we defined above.
Common error
If you are getting an EXC_BAD_ACCESS error, it is probably because you set the view's custom class as Keyboard.swift rather than do this for the nib File's Owner.
Select Keyboard.nib and then choose File's Owner.
Make sure that the custom class for the root view is blank.
The key is to use the existing UIKeyInput protocol, to which UITextField already conforms. Then your keyboard view need only to send insertText() and deleteBackward() to the control.
The following example creates a custom numeric keyboard:
class DigitButton: UIButton {
var digit: Int = 0
}
class NumericKeyboard: UIView {
weak var target: (UIKeyInput & UITextInput)?
var useDecimalSeparator: Bool
var numericButtons: [DigitButton] = (0...9).map {
let button = DigitButton(type: .system)
button.digit = $0
button.setTitle("\($0)", for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
button.setTitleColor(.black, for: .normal)
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor.darkGray.cgColor
button.accessibilityTraits = [.keyboardKey]
button.addTarget(self, action: #selector(didTapDigitButton(_:)), for: .touchUpInside)
return button
}
var deleteButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("⌫", for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
button.setTitleColor(.black, for: .normal)
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor.darkGray.cgColor
button.accessibilityTraits = [.keyboardKey]
button.accessibilityLabel = "Delete"
button.addTarget(self, action: #selector(didTapDeleteButton(_:)), for: .touchUpInside)
return button
}()
lazy var decimalButton: UIButton = {
let button = UIButton(type: .system)
let decimalSeparator = Locale.current.decimalSeparator ?? "."
button.setTitle(decimalSeparator, for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
button.setTitleColor(.black, for: .normal)
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor.darkGray.cgColor
button.accessibilityTraits = [.keyboardKey]
button.accessibilityLabel = decimalSeparator
button.addTarget(self, action: #selector(didTapDecimalButton(_:)), for: .touchUpInside)
return button
}()
init(target: UIKeyInput & UITextInput, useDecimalSeparator: Bool = false) {
self.target = target
self.useDecimalSeparator = useDecimalSeparator
super.init(frame: .zero)
configure()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - Actions
extension NumericKeyboard {
#objc func didTapDigitButton(_ sender: DigitButton) {
insertText("\(sender.digit)")
}
#objc func didTapDecimalButton(_ sender: DigitButton) {
insertText(Locale.current.decimalSeparator ?? ".")
}
#objc func didTapDeleteButton(_ sender: DigitButton) {
target?.deleteBackward()
}
}
// MARK: - Private initial configuration methods
private extension NumericKeyboard {
func configure() {
autoresizingMask = [.flexibleWidth, .flexibleHeight]
addButtons()
}
func addButtons() {
let stackView = createStackView(axis: .vertical)
stackView.frame = bounds
stackView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(stackView)
for row in 0 ..< 3 {
let subStackView = createStackView(axis: .horizontal)
stackView.addArrangedSubview(subStackView)
for column in 0 ..< 3 {
subStackView.addArrangedSubview(numericButtons[row * 3 + column + 1])
}
}
let subStackView = createStackView(axis: .horizontal)
stackView.addArrangedSubview(subStackView)
if useDecimalSeparator {
subStackView.addArrangedSubview(decimalButton)
} else {
let blank = UIView()
blank.layer.borderWidth = 0.5
blank.layer.borderColor = UIColor.darkGray.cgColor
subStackView.addArrangedSubview(blank)
}
subStackView.addArrangedSubview(numericButtons[0])
subStackView.addArrangedSubview(deleteButton)
}
func createStackView(axis: NSLayoutConstraint.Axis) -> UIStackView {
let stackView = UIStackView()
stackView.axis = axis
stackView.alignment = .fill
stackView.distribution = .fillEqually
return stackView
}
func insertText(_ string: String) {
guard let range = target?.selectedRange else { return }
if let textField = target as? UITextField, textField.delegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) == false {
return
}
if let textView = target as? UITextView, textView.delegate?.textView?(textView, shouldChangeTextIn: range, replacementText: string) == false {
return
}
target?.insertText(string)
}
}
// MARK: - UITextInput extension
extension UITextInput {
var selectedRange: NSRange? {
guard let textRange = selectedTextRange else { return nil }
let location = offset(from: beginningOfDocument, to: textRange.start)
let length = offset(from: textRange.start, to: textRange.end)
return NSRange(location: location, length: length)
}
}
Then you can:
textField.inputView = NumericKeyboard(target: textField)
That yields:
Or, if you want a decimal separator, too, you can:
textField.inputView = NumericKeyboard(target: textField, useDecimalSeparator: true)
The above is fairly primitive, but it illustrates the idea: Make you own input view and use the UIKeyInput protocol to communicate keyboard input to the control.
Also please note the use of accessibilityTraits to get the correct “Spoken Content” » “Speak Screen” behavior. And if you use images for your buttons, make sure to set accessibilityLabel, too.
Building on Suragch's answer, I needed a done and backspace button and if you're a noob like me heres some errors you might encounter and the way I solved them.
Getting EXC_BAD_ACCESS errors?
I included:
#objc(classname)
class classname: UIView{
}
fixed my issue however Suragch's updated answer seems to solve this the more appropriate/correct way.
Getting SIGABRT Error?
Another silly thing was dragging the connections the wrong way, causing SIGABRT error. Do not drag from the function to the button but instead the button to the function.
Adding a Done Button
I added this to the protocol in keyboard.swift:
protocol KeyboardDelegate: class {
func keyWasTapped(character: String)
func keyDone()
}
Then connected a new IBAction from my done button to keyboard.swift like so:
#IBAction func Done(sender: UIButton) {
self.delegate?.keyDone()
}
and then jumped back to my viewController.swift where i am using this keyboard and added this following after the function keyWasTapped:
func keyDone() {
view.endEditing(true)
}
Adding Backspace
This tripped me up a lot, because you must set the textField.delegate to self in the viewDidLoad() method (shown later).
First: In keyboard.swift add to the protocol func backspace():
protocol KeyboardDelegate: class {
func keyWasTapped(character: String)
func keyDone()
func backspace()
}
Second: Connect a new IBAction similar to the Done action:
#IBAction func backspace(sender: UIButton) {
self.delegate?.backspace()
}
Third: Over to the viewController.swift where the NumberPad is appearing.
Important: In viewDidLoad() set all textFields that will be using this keyboard. So your viewDidLoad() should look something like this:
override func viewDidLoad() {
super.viewDidLoad()
self.myTextField1.delegate = self
self.myTextField2.delegate = self
// initialize custom keyboard
let keyboardView = keyboard(frame: CGRect(x: 0, y: 0, width: 0, height: 240))
keyboardView.delegate = self // the view controller will be notified by the keyboard whenever a key is tapped
// replace system keyboard with custom keyboard
myTextField1.inputView = keyboardView
myTextField2.inputView = keyboardView
}
I'm not sure how to, if there is a way to just do this to all textFields that are in the view. This would be handy...
Forth: Still in viewController.swift we need to add a variable and two functions. It will look like this:
var activeTextField = UITextField()
func textFieldDidBeginEditing(textField: UITextField) {
print("Setting Active Textfield")
self.activeTextField = textField
print("Active textField Set!")
}
func backspace() {
print("backspaced!")
activeTextField.deleteBackward()
}
Explanation of whats happening here:
You make a variable that will hold a textField.
When the "textFieldDidBeginEditing" is called it sets the variable so it knows which textField we are dealing with. I've added a lot of prints() so we know everything is being executed.
Our backspace function then checks the textField we are dealing with and uses .deleteBackward(). This removes the immediate character before the cursor.
And you should be in business.
Many thanks to Suragchs for helping me get this happening.
I'm creating pages dynamically, each page contains a Navigation Controller and UIViewController.
Inside each page, there are components like link, images, texts.
Each component is a class like the following:
class link: Component, ComponentProtocol {
var text: String
var url: String
func browseURL(sender: UIButton!){
let targetURL = NSURL(fileURLWithPath: self.url)
let application = UIApplication.sharedApplication()
application.openURL(targetURL!)
}
func generateView() -> UIView?{
var result: UIView?
var y = CGRectGetMinY(frame)
var linkBtn = UIButton(frame: CGRect(x: 0, y:30 , width:300 , height: 50)
linkBtn.setTitle(self.text, forState: UIControlState.Normal)
linkBtn.setTitleColor(UIColor.blueColor(), forState: UIControlState.Normal)
linkBtn.titleLabel?.font = linkBtn.titleLabel?.font.fontWithSize(15)
// this doesn't seem to be registered properly
linkBtn.addTarget(self, action: "browseURL:", forControlEvents: UIControlEvents.TouchUpInside)
result = UIView()
result?.addSubview(linkBtn)
return result
}
Then in the ViewDidLoad of the page ViewController method I would have this to initialise the page components:
for component in components!{
if let acomponent:ComponentProtocol = component as? ComponentProtocol {
if let res = acomponent.generateView(innerFrame) {
if let view = res.view {
self.view.addSubview(view)
}
}
}
}
The button is showing, but when I touch nothing happens. When I debugged, the browseURL is no triggered at all.
What's wrong with my code? I'm guessing because I registered the action in link class and not in the ViewController of the page?
UPDATE
This could be a similar issue, but the answer is not so straightforward and I actually have the component reference in my ViewController: (Target: Object) not working for setting UIButton from outside viewController
I'm not at a Mac right now so I can't test, but does this work:
linkBtn.target = self
linkBtn.action = "browseURL:"
I think if you have selectors in Swift you have to be careful with colons and specifying #objc if your class doesn't inherit from NSObject, it could also be something to do with that?