swift functions with default parameters also a selector? - ios

I wanted to be able to call this function from two places: When I finish editing a text field, I want to add a new webView when there are none in a stackView, and I also want to be able to use a barButtonItem to do so.
I'm having two problems. when the bar button calls this function, the parameter 'url', becomes an object, type UIBarButtonItem. when it's called from textFieldShouldReturn, it properly comes in as an NSURL. if the user doesn't type anything in the address field, and hits enter, a blank NSURL comes in, and the default value is not used. (i'd like it to be)
what should the call look like from the textfieldShouldReturn function, so that a blank will trigger the default?
how do i handle the fact that either my function or the button will call the function, and why does my named parameter 'url' become what i guess would be 'sender?'
override func viewDidLoad() {
super.viewDidLoad()
setDefaultTitle()
let add = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: #selector(ViewController.addWebView))
let delete = UIBarButtonItem(barButtonSystemItem: .Trash, target: self, action: #selector(ViewController.deleteWebView))
navigationItem.rightBarButtonItems = [delete, add]
}
func addWebView(url: NSURL = NSURL(string: "https://www.google.com")!) {
let webView = UIWebView()
webView.delegate = self
stackView .addArrangedSubview(webView)
webView.loadRequest(NSURLRequest(URL: url))
webView.layer.borderColor = UIColor.blueColor().CGColor
selectWebView(webView)
let recognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.webViewTapped))
recognizer.delegate = self
webView.addGestureRecognizer(recognizer)
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
if let webView = activeWebView, address = addressBar.text {
if let url = NSURL(string: address) {
webView.loadRequest(NSURLRequest(URL: url))
}
} else if stackView.arrangedSubviews.count == 0 {
let address = NSURL(string: addressBar.text!)!
addWebView(address)
}
textField.resignFirstResponder()
return true
}

That's right that you are getting sender object which is actually UIBarButtonItem. Have you heard about Target-Action Cocoa pattern? If no, you can read more here:
https://developer.apple.com/library/ios/documentation/General/Conceptual/Devpedia-CocoaApp/TargetAction.html
Especially relevant section to you is "An Action Method Must Have a Certain Form".
Consider to introduce addWebView overload:
func addWebView(sender: NSObject) {
addWebView(url: NSURL(string: "https://www.google.com")!)
}
private func addWebView(url: NSURL) {
//left as is ...
}
Here is update per Dave's comments.
Have to use different name for actual implementation method. Otherwise Swift compiler is failed to resolve the assigned selector name.
Useful code, which demonstrates the problem is attached below:
class Notifier: NSObject {
private var _target: NSObject!
private var _action: Selector!
func addObserver(target: NSObject, action: Selector) {
_target = target
_action = action
}
func invokeMethod() {
guard let t = _target else {
print("target must be set")
return
}
guard let a = _action else {
print("action must be set")
return
}
if t.respondsToSelector(a) {
t.performSelector(a, withObject: self)
}
}
}
class Observer: NSObject {
func subscribe(notifier: Notifier) {
notifier.addObserver(self, action: #selector(Observer.callback))
}
func callback(sender: NSObject) {
callbackImpl(NSURL(string: "https://www.google.com")!)
}
private func callbackImpl(url: NSURL) {
print("url\(url)")
}
}
//client's code
let n = Notifier()
let o = Observer()
o.subscribe(n)
n.invokeMethod()

Related

Find UIRefreshControl in UI Test

I am wanting to test the existence of a UIRefreshControl inside a UI Test. I define my control all init:
itemRefreshControl.accessibilityIdentifier = "MyRefreshIndicator"
// allow UITest to find the refresh
if let refreshLabel = itemRefreshControl.subviews.first?.subviews.last as? UILabel {
refreshLabel.isAccessibilityElement = true
refreshLabel.accessibilityIdentifier = "MyRefreshLabel"
}
Then in my test case I have tried:
let refreshCtlQuery = NSPredicate(format: "label CONTAINS[c] 'Refreshing'")
let refreshControl = app.staticTexts.containing(refreshCtlQuery)
expectation(for: exists, evaluatedWith: refreshControl, handler: nil)
start.press(forDuration: 0, thenDragTo: finish)
print(app.debugDescription)
waitForExpectations(timeout: 5, handler: nil)
I also tried:
let refreshControl = app.staticTexts["MyRefreshLabel"]
and I tried:
let refreshControl = app.activityIndicators["MyRefreshIndicator"]
In all those cases I can see the test runner perform the drag and I see the refresh control in the UI, but the expectation always fails. It's almost like the test blocks until the refreshing is done and then checks for existence and it's not there. When I print out the view hierarchy, I can't find the UIRefreshControl's label. How best can I test this?
Indeed, while UIRefreshControl does its animation, the tests are hang up saying "Wait for <BUNDLE_IDENTIFIER> to idle".
You can swizzle XCUIApplicationProcess.waitForQuiescenceIncludingAnimationsIdle: to an empty method, so you can bypass this behaviour (based on this answer).
extension XCTestCase {
static var disabledQuiescenceWaiting = false
/// Swizzle `XCUIApplicationProcess.waitForQuiescenceIncludingAnimationsIdle(:)`
/// to empty method. Invoke at `setUpWithError()` of your test case.
func disableQuiescenceWaiting() {
// Only if not disabled yet.
guard Self.disabledQuiescenceWaiting == false else { return }
// Swizzle.
if
let `class`: AnyClass = objc_getClass("XCUIApplicationProcess") as? AnyClass,
let quiescenceWaitingMethod = class_getInstanceMethod(`class`, Selector(("waitForQuiescenceIncludingAnimationsIdle:"))),
let emptyMethod = class_getInstanceMethod(type(of: self), #selector(Self.empty))
{
method_exchangeImplementations(quiescenceWaitingMethod, emptyMethod)
Self.disabledQuiescenceWaiting = true
}
}
#objc func empty() {
return
}
}
Then call at the setup of your test case.
override func setUpWithError() throws {
disableQuiescenceWaiting()
}
You can mark the UIRefreshControl directly (I made this extension for convenience).
extension UIRefreshControl {
func testable(as id: String) -> UIRefreshControl {
self.isAccessibilityElement = true
self.accessibilityIdentifier = id
return self
}
}
// Create the instance like this.
UIRefreshControl().testable(as: "RefreshControl")
So you can nicely assert the existence (get from otherElements).
let refreshControlElement = app.otherElements["RefreshControl"]
XCTAssertTrue(refreshControlElement.waitForExistence(timeout: 1))

Why canPerformAction get called again when action of menuItem is called?

Below is my code, I found when click menu "pasteAndGo", two log strings are printed: 1. paste and go show 2.paste and go clicked. My requirement is when the menu is shown, log "paste and go show" is shown. When it is clicked, log "paste and go clicked" is shown.
class MyTextField: UITextField {
private func Init() {
let menuController: UIMenuController = UIMenuController.shared
menuController.isMenuVisible = true
let pasteAndGoMenuItem: UIMenuItem = UIMenuItem(title: "pasteAndGo", action: #selector(pasteAndGo(sender:)))
let myMenuItems: NSArray = [pasteAndGoMenuItem]
menuController.menuItems = myMenuItems as? [UIMenuItem]
}
#objc private func pasteAndGo(sender: UIMenuItem) {
Print("paste and go clicked")
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
let pasteboard = UIPasteboard.general
if action == #selector(pasteAndGo) {
if pasteboard.url != nil {
Print("paste and go show")
return true
} else {
return false
}
}
return super.canPerformAction(action, withSender: sender)
}
}
Your code works as implemented:
In the instant you press your pasteAndGo menu item, the UIKit framework calls canPerformAction to ask whether it is allowed to execute the action or not. Here, you print "paste and go show"
Since you return true, your action pasteAndGo(sender:) is executed and prints "paste and go clicked"
To react on the menu item being shown, you'll have to register to the notification center with the UIMenuController.willShowMenuNotification, like this:
// create a property
var token: NSObjectProtocol?
// then add observer
self.token = NotificationCenter.default.addObserver(forName: UIMenuController.willShowMenuNotification, object: nil, queue: .main)
{ _ in
print ("paste and go show")
}
and don't forget to unsubscribe (NotificationCenter.default.removeObserver) once your viewcontroller gets dismissed.
if let t = self.token {
NotificationCenter.default.removeObserver(t)
}
Update
You could also do so (without properties) in Init
// in Init
var token = NotificationCenter.default.addObserver(forName: UIMenuController.willShowMenuNotification, object: nil, queue: .main)
{ _ in
print ("paste and go show")
NotificationCenter.default.removeObserver(token)
}

Passing arguments to selector in Swift

I'm programmatically adding a UITapGestureRecognizer to one of my views:
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(modelObj:myModelObj)))
self.imageView.addGestureRecognizer(gesture)
func handleTap(modelObj: Model) {
// Doing stuff with model object here
}
The first problem I encountered was "Argument of '#selector' does not refer to an '#Objc' method, property, or initializer.
Cool, so I added #objc to the handleTap signature:
#objc func handleTap(modelObj: Model) {
// Doing stuff with model object here
}
Now I'm getting the error "Method cannot be marked #objc because the type of the parameter cannot be represented in Objective-C.
It's just an image of the map of a building, with some pin images indicating the location of points of interest. When the user taps one of these pins I'd like to know which point of interest they tapped, and I have a model object which describes these points of interest. I use this model object to give the pin image it's coordinates on the map so I thought it would have been easy for me to just send the object to the gesture handler.
It looks like you're misunderstanding a couple of things.
When using target/action, the function signature has to have a certain form…
func doSomething()
or
func doSomething(sender: Any)
or
func doSomething(sender: Any, forEvent event: UIEvent)
where…
The sender parameter is the control object sending the action message.
In your case, the sender is the UITapGestureRecognizer
Also, #selector() should contain the func signature, and does NOT include passed parameters. So for…
func handleTap(sender: UIGestureRecognizer) {
}
you should have…
let gesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(sender:)))
Assuming the func and the gesture are within a view controller, of which modelObj is a property / ivar, there's no need to pass it with the gesture recogniser, you can just refer to it in handleTap
Step 1: create the custom object of the sender.
step 2: add properties you want to change in that a custom object of the sender
step 3: typecast the sender in receiving function to a custom object and access those properties
For eg:
on click of the button if you want to send the string or any custom object then
step 1: create
class CustomButton : UIButton {
var name : String = ""
var customObject : Any? = nil
var customObject2 : Any? = nil
convenience init(name: String, object: Any) {
self.init()
self.name = name
self.customObject = object
}
}
step 2-a: set the custom class in the storyboard as well
step 2-b: Create IBOutlet of that button with a custom class as follows
#IBOutlet weak var btnFullRemote: CustomButton!
step 3: add properties you want to change in that a custom object of the sender
btnFullRemote.name = "Nik"
btnFullRemote.customObject = customObject
btnFullRemote.customObject2 = customObject2
btnFullRemote.addTarget(self, action: #selector(self.btnFullRemote(_:)), for: .touchUpInside)
step 4: typecast the sender in receiving function to a custom object and access those properties
#objc public func btnFullRemote(_ sender: Any) {
var name : String = (sender as! CustomButton).name as? String
var customObject : customObject = (sender as! CustomButton).customObject as? customObject
var customObject2 : customObject2 = (sender as! CustomButton).customObject2 as? customObject2
}
Swift 5.0 iOS 13
I concur a great answer by Ninad. Here is my 2 cents, the same and yet different technique; a minimal version.
Create a custom class, throw a enum to keep/make the code as maintainable as possible.
enum Vs: String {
case pulse = "pulse"
case precision = "precision"
}
class customTap: UITapGestureRecognizer {
var cutomTag: String?
}
Use it, making sure you set the custom variable into the bargin. Using a simple label here, note the last line, important labels are not normally interactive.
let precisionTap = customTap(target: self, action: #selector(VC.actionB(sender:)))
precisionTap.customTag = Vs.precision.rawValue
precisionLabel.addGestureRecognizer(precisionTap)
precisionLabel.isUserInteractionEnabled = true
And setup the action using it, note I wanted to use the pure enum, but it isn't supported by Objective C, so we go with a basic type, String in this case.
#objc func actionB(sender: Any) {
// important to cast your sender to your cuatom class so you can extract your special setting.
let tag = customTag as? customTap
switch tag?.sender {
case Vs.pulse.rawValue:
// code
case Vs.precision.rawValue:
// code
default:
break
}
}
And there you have it.
cell.btn.tag = indexPath.row //setting tag
cell.btn.addTarget(self, action: #selector(showAlert(_ :)), for: .touchUpInside)
#objc func showAlert(_ sender: UIButton){
print("sender.tag is : \(sender.tag)")// getting tag's value
}
Just create a custom class of UITapGestureRecognizer =>
import UIKit
class OtherUserProfileTapGestureRecognizer: UITapGestureRecognizer {
let userModel: OtherUserModel
init(target: AnyObject, action: Selector, userModel: OtherUserModel) {
self.userModel = userModel
super.init(target: target, action: action)
}
}
And then create UIImageView extension =>
import UIKit
extension UIImageView {
func gotoOtherUserProfile(otherUserModel: OtherUserModel) {
isUserInteractionEnabled = true
let gestureRecognizer = OtherUserProfileTapGestureRecognizer(target: self, action: #selector(self.didTapOtherUserImage(_:)), otherUserModel: otherUserModel)
addGestureRecognizer(gestureRecognizer)
}
#objc internal func didTapOtherUserImage(_ recognizer: OtherUserProfileTapGestureRecognizer) {
Router.shared.gotoOtherUserProfile(otherUserModel: recognizer.otherUserModel)
}
}
Now use it like =>
self.userImageView.gotoOtherUserProfile(otherUserModel: OtherUserModel)
You can use an UIAction instead:
self.imageView.addAction(UIAction(identifier: UIAction.Identifier("imageClick")) { [weak self] action in
self?.handleTap(modelObj)
}, for: .touchUpInside)
that may be a terrible practice but I simply add whatever I want to restore to
button.restorationIdentifier = urlString
and
#objc func openRelatedFact(_ sender: Any) {
if let button = sender as? UIButton, let stringURL = factButton.restorationIdentifier, let url = URL(string: stringURL) {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:])
}
}
}

UIMenuItem disabled auto selector on method

I have a problem with create new UIMenuItem and assigning it a selector. Problem is it automatically call its selector without tapping it.
This is my code:
let customMenuItem1 = UIMenuItem(title: "Salvează", action: Selector(showNote()))
menuController.menuItems = NSArray(array: [customMenuItem1]) as? [UIMenuItem]
This is the method for appearance of menuitem:
override func canPerformAction(action: Selector,withSender sender: AnyObject?) -> Bool
{
if action == Selector(showNote())
{
return super.canPerformAction(action, withSender: sender)
}
return false
}
Thanks all.
In first 2 line of code exist mistake in swift:
let customMenuItem1 = UIMenuItem(title: "Salvează", action: Selector(showNote()))
menuController.menuItems = NSArray(array: [customMenuItem1]) as? [UIMenuItem]
at hear we have Selector on method this Selector mean he will be aumatically call method without wait for user to tap and for resolving this problem only we can put like this
let customMenuItem1 = UIMenuItem(title: "Salvează", action: #selector(RulesDetailViewController.showNote))
menuController.menuItems = NSArray(array: [customMenuItem1]) as? [UIMenuItem]
because #selector this parameters wait for touch and event for users.

iOS Keyboard: textDocumentProxy.documentContextBeforeInput.isEmpty Unexpectedly Returning Nil (when it shouldn't)

I'm working on a custom iOS keyboard – much of it is in place, but I'm having trouble with the delete key and more specifically with deleting whole words.
The issue is that self.textDocumentProxy.documentContextBeforeInput?.isEmpty returns Nil even if there are characters remaining in the text field.
Here's the background:
In case you're not familiar, the way it works on the stock iOS keyboard is that while holding the backspace key, the system deletes a character at a time (for the first ~10 characters). After 10 characters, it starts deleting whole words.
In the case of my code, I'm deleting 10 single characters and then I successfully delete a couple of whole words, then suddenly self.textDocumentProxy.documentContextBeforeInput?.isEmpty returns Nil, even if there are characters remaining in the text field.
I've looked all over documentation and the web and I don't see anyone else with the same issue, so I'm sure I'm missing something obvious, but I'm baffled.
Here are the relevant parts of my class definition:
class KeyboardViewController: UIInputViewController {
//I've removed a bunch of variables that aren't relevant to this question.
var myInputView : UIInputView {
return inputView!
}
private var proxy: UITextDocumentProxy {
return textDocumentProxy
}
override func canBecomeFirstResponder() -> Bool {
return true
}
override func viewDidLoad() {
super.viewDidLoad()
//proxy let proxy = self.textDocumentProxy
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillAppear"), name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillHide"), name: UIKeyboardWillHideNotification, object: nil)
self.myInputView.translatesAutoresizingMaskIntoConstraints = true
// Perform custom UI setup here
view.backgroundColor = UIColor(red: 209 / 255, green: 213 / 255, blue: 219 / 255, alpha: 1)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "touchUpInsideLetter:", name: "KeyboardKeyPressedNotification", object: nil)
showQWERTYKeyboard()
}
I setup the listeners and actions for the backspace button as I'm also configuring a bunch of other attributes on the button. I do that in a Switch – the relevant part is here:
case "<<":
isUIButton = true
normalButton.setTitle(buttonString, forState: UIControlState.Normal)
normalButton.addTarget(self, action: "turnBackspaceOff", forControlEvents: UIControlEvents.TouchUpInside)
normalButton.addTarget(self, action: "turnBackspaceOff", forControlEvents: UIControlEvents.TouchUpOutside)
normalButton.addTarget(self, action: "touchDownBackspace", forControlEvents: UIControlEvents.TouchDown)
let handleBackspaceRecognizer : UILongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: "handleBackspaceLongPress:")
normalButton.addGestureRecognizer(handleBackspaceRecognizer)
buttonWidth = standardButtonWidth * 1.33
nextX = nextX + 7
Thanks for any thought you can offer.
****Modifying the original post to help shed more light on the issue****
Here are the 4 functions that are designed to create the backspace behavior. They appear to work properly for at least the first two whole words, but then the optional checking that was correctly suggested starts evaluating to nil and it stops deleting.
//Gets called on "Delete Button TouchUpInside" and "Delete Button TouchUpOutside"
func turnBackspaceOff() {
self.backspaceIsPressed = false
keyRepeatTimer.invalidate()
}
//Handles a single tap backspace
func touchDownBackspace() {
(textDocumentProxy as UIKeyInput).deleteBackward()
}
//Handles a long press backspace
func handleBackspaceLongPress(selector : UILongPressGestureRecognizer) {
if selector.state == UIGestureRecognizerState.Began {
self.backspaceIsPressed = true
self.keyRepeatTimer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: "backspaceRepeatHandlerFinal", userInfo: nil, repeats: true)
print("handleBackspaceLongPress.Began")
}
else if selector.state == UIGestureRecognizerState.Ended {
self.backspaceIsPressed = false
keyRepeatTimer.invalidate()
numberOfKeyPresses = 0
print("handleBackspaceLongPress.Ended")
}
else {
self.backspaceIsPressed = false
keyRepeatTimer.invalidate()
numberOfKeyPresses = 0
print("handleBackspaceLongPress. Else")
}
}
func backspaceRepeatHandlerFinal() {
if let documentContext = proxy.documentContextBeforeInput as String? {
print(documentContext)
}
print("backspaceRepeatHandlerFinal is called")
if self.backspaceIsPressed {
print("backspace is pressed")
self.numberOfKeyPresses = self.numberOfKeyPresses + 1
if self.numberOfKeyPresses < 10 {
proxy.deleteBackward()
}
else {
if let documentContext = proxy.documentContextBeforeInput as NSString? {
let tokens : [String] = documentContext.componentsSeparatedByString(" ")
var i : Int = Int()
for i = 0; i < String(tokens.last!).characters.count + 1; i++ {
(self.textDocumentProxy as UIKeyInput).deleteBackward()
}
}
else {
print("proxy.documentContextBeforeInput was nil")
self.keyRepeatTimer.invalidate()
self.numberOfKeyPresses = 0
}
}
}
else {
print("In the outer else")
self.keyRepeatTimer.invalidate()
self.numberOfKeyPresses = 0
}
}
Finally, I don't fully understand why, but XCode automatically inserted these two functions below when I created the keyboard extension. I've modified them slightly in an effort to try to get this to work.
override func textWillChange(textInput: UITextInput?) {
// The app is about to change the document's contents. Perform any preparation here.
super.textWillChange(textInput)
}
override func textDidChange(textInput: UITextInput?) {
// The app has just changed the document's contents, the document context has been updated.
var textColor: UIColor
//let proxy = self.textDocumentProxy
if proxy.keyboardAppearance == UIKeyboardAppearance.Dark {
textColor = UIColor.whiteColor()
} else {
textColor = UIColor.blackColor()
}
super.textDidChange(textInput)
}
Let's describe what the line below is doing:
if !((self.textDocumentProxy.documentContextBeforeInput?.isEmpty) == nil) {
First, it takes an object marked as optional (by the ? letter):
let documentContext = self.textDocumentProxy.documentContextBeforeInput
Then, it tries to read it's property called isEmpty:
let isEmpty = documentContext?.isEmpty
and then it evaluates the contition:
if !(isEmpty == nil) {
There are two mistakes. The first one is that you are comparing Bool value with nil. Another one is that you aren't sure that documentContext is not a nil.
So, let's write your code in a more appropriate way:
if let documentContext = self.textDocumentProxy.documentContextBeforeInput { // Make sure that it isn't nil
if documentContext.isEmpty == false { // I guess you need false?
// Do what you want with non-empty document context
}
}

Resources