I currently have an action sheet made using SimpleAlert that generates a list of buttons. The buttons recognize taps and long presses. On the long press I am trying to pass the button as a sender through a selector in order to access the button tag in another function, however it keeps giving me this error:
2017-07-26 11:27:15.173 hitBit[8614:134456] *** Terminating app due to
uncaught exception 'NSInvalidArgumentException', reason:
'-[LongTap(sender: button]: unrecognized selector sent to instance
0x7f9c458ccc00'
How can I pass objects such as buttons through the selector? If there is a solution that allows me to just pass through an integer, that would work fine as well.
#IBAction func tapMGName(_ sender: Any) {
let mgController = MouthguardSelectionController(title: "Go to:", message: nil, style: .actionSheet)
//For every MG, make an action that will navigate you to the mouthguard selected
for i in 0...self.getNumberDevices() - 1 {
mgController.addAction(index: i, (AlertAction(title: mg_Name[i], style: .ok) { action -> Void in
self.changeMouthguard(index: i)
self.dismiss(animated: true, completion: nil)
}))
}
Code that creates the custom action sheet and generates actions for the list
override func configureButton(_ style :AlertAction.Style, forButton button: UIButton, index: Int) {
super.configureButton(style, forButton: button)
cur_mg_ID_index = index
let longGesture = UILongPressGestureRecognizer(target: self, action: "LongTap(sender: button") //Long function will call when user long press on button.
if (button.titleLabel?.font) != nil {
switch style {
case .ok:
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 20)
button.tag = index
button.addGestureRecognizer(longGesture)
case .cancel:
button.backgroundColor = UIColor.darkGray
button.setTitleColor(UIColor.white, for: .normal)
case .default:
button.setTitleColor(UIColor.lightGray, for: .normal)
default:
break
}
}
}
func LongTap(sender: UIButton) {
print(sender.tag)
let nameChanger = AlertController(title: "Change name of ya boy", message: nil, style: .alert)
nameChanger.addTextFieldWithConfigurationHandler() { textField in
textField?.frame.size.height = 33
textField?.backgroundColor = nil
textField?.layer.borderColor = nil
textField?.layer.borderWidth = 0
}
nameChanger.addAction(.init(title: "Cancel", style: .cancel))
nameChanger.addAction(.init(title: "OK", style: .ok))
present(nameChanger, animated: true, completion: nil)
}
Code within the custom SimpleAlert action sheet
Try this and see,
override func configureButton(_ style :AlertAction.Style, forButton button: UIButton, index: Int) {
super.configureButton(style, forButton: button)
cur_mg_ID_index = index
// Edited line....
let longGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.longTap(_:))) //Long function will call when user long press on button.
if (button.titleLabel?.font) != nil {
switch style {
case .ok:
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 20)
button.tag = index
button.addGestureRecognizer(longGesture)
case .cancel:
button.backgroundColor = UIColor.darkGray
button.setTitleColor(UIColor.white, for: .normal)
case .default:
button.setTitleColor(UIColor.lightGray, for: .normal)
default:
break
}
}
}
// Edited line....
func longTap(_ gesture: UILongPressGestureRecognizer) {
// Edited line....
guard let sender = gesture.view as? UIButton else {
print("Sender is not a button")
return
}
print(sender.tag)
let nameChanger = AlertController(title: "Change name of ya boy", message: nil, style: .alert)
nameChanger.addTextFieldWithConfigurationHandler(){textField in
textField?.frame.size.height = 33
textField?.backgroundColor = nil
textField?.layer.borderColor = nil
textField?.layer.borderWidth = 0
}
nameChanger.addAction(.init(title: "Cancel", style: .cancel))
nameChanger.addAction(.init(title: "OK", style: .ok))
present(nameChanger, animated: true, completion: nil)
}
Related
how can I create reusable view controller (let's call it "reusableVC") acting like UIAlertController. ReusableVC have "ok" button, that will act depending from where resuableVC called. I know about delegates and NotificationCenter. Just wondering can we pass what "ok" button should do when creating reusableVC, like this:
reusableVC.addAction(UIAlertAction(title: "", style: .default, handler: { (action) in
// some code
}))
If you only need one OK button you may use this solution, otherwise, you can still find interest in this pattern.
class ReusableVC{
var onOKPressed: ( () -> () )?
// Create all your other things and don't forget that you should call onOKPressed() whenever user pushed that OK button
}
class ViewController{
func setupReusableVC(){
let reusableVC = ReusableVC()
reusableVC.onOKPressed = {
print("ok pressed")
}
}
}
The action handler is just a closure. You can declare it everywhere.
In the reusable view controller add a property
var customAction : ((UIAlertAction) -> Void)?
and pass the property as handler
reusableVC.addAction(UIAlertAction(title: "", style: .default, handler: customAction))
In the source view controller create the action
let action : ((UIAlertAction) -> Void)? = { action in
// do something
}
and pass it in perform(segue
Create a UIViewController Extension to include Alert Functionality
extension UIViewController{
open func hideKeyBoardOnTap(){
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
self.view.addGestureRecognizer(tap)
}
#objc private func dismissKeyboard(){
self.view.endEditing(true)
}
open func showAlertWithOK(_ title: String = "Alert!",message: String = "Please take appropriate action"){
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
let okButton = UIAlertAction(title: "Ok", style: .default, handler:{ (alertAction) in
self.okAction()
})
alert.addAction(okButton)
DispatchQueue.main.async {
self.present(alert, animated: true, completion: nil)
}
}
open func showAlertWithOkAndCancel(_ title: String = "Alert!",_ message: String = "Please take appropriate action", _ firstButtonTitle: String = "Ok", _ firstButtonStyle: UIAlertActionStyle = .default, _ secondButtonTitle: String = "Cancel",_ secondButtonStyle: UIAlertActionStyle = .cancel){
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
let okButton = UIAlertAction(title: firstButtonTitle, style: firstButtonStyle, handler:{ (alertAction) in
self.okAction()
})
let cancelButton = UIAlertAction(title: secondButtonTitle, style: secondButtonStyle, handler: { (alertAction) in
self.cancelAction()
})
alert.addAction(okButton)
alert.addAction(cancelButton)
self.present(alert, animated: true, completion: nil)
}
#objc private func okAction(){
self.dismiss(animated: true, completion: nil)
}
#objc private func cancelAction(){
self.dismiss(animated: true, completion: nil)
}
}
How to Use
func didReceiveError(_ error: CFNetworkErrors) {
var message = error.message
self.showAlertWithOK("Error", message: message)
}
func didEndWebserviceCall() {
self.showAlertWithOK(message: "didEndWebserviceCall")
}
Advantages:
You can access alert using self(which is your viewcontroller in this case)
Code reusability
Clean code.
protocol TapEventDelegate: protocol {
func buttonTap()
}
class ClassWhereDoYouWantToCatchTheEvent: TapEventDelegate {
func buttonTap() {
print("caught!")
}
}
class YourViewControllerClass {
weak var tapEventDelegate: TapEventDelegate?
reusableVC.addAction(UIAlertAction(title: "", style: .default, handler: { (action) in
tapEventDelegate?.buttonTap()
}))
}
to bind your class with YourViewControllerClass and ClassWhereDoYouWantToCatchTheEvent use somewhere at view controller initialization:
classWhereDoYouWantToCatchTheEvent.tapEventHandler = yourViewControllerClass
You can create custom UIViewController class and pass the addAction closure and then you can call that closure on the OK button tap from your CustomAlertController.
final class CustomAlertController: UIViewController {
var actionHandler: (() -> Void)?
lazy var okButton: UIButton = {
let button = UIButton()
button.backgroundColor = .black
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("OK", for: .normal)
button.addTarget(self, action: #selector(CustomAlertController.didTapOkButton(_:)), for: .touchUpInside)
button.layer.cornerRadius = 10
return button
}()
override func loadView() {
view = UIView()
view.backgroundColor = .white
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
addActionButton()
}
private func addActionButton() {
view.addSubview(okButton)
NSLayoutConstraint.activate([
okButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 50),
okButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -50),
okButton.heightAnchor.constraint(equalToConstant: 50),
okButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 100)
])
}
public func addAction(title: String, handler: #escaping (() -> Void) {
okButton.setTitle(title, for: .normal)
actionHandler = handler
}
#objc func didTapOkButton(_ button: UIButton) {
actionHandler?()
dismiss(animated: true)
}
}
Then you can present CustomAlertController from your ViewController class and add action like below
class ViewController: UIViewController {
override func loadView() {
view = UIView()
view.backgroundColor = .white
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let alertController = CustomAlertController()
alertController.addAction(title: "OK", handler: { [unowned self] in
self.view.backgroundColor = .blue
print("OK button tapped")
})
present(alertController, animated: true)
}
}
I have the following function that adds next and previous buttons that will switch between textfields to allow a better user experience. However I am not able to implement this through the textfields that are within the alert controller. Is there a solution or must I think of another design for my app?
extension UIViewController {
func addInputAccessoryForTextFields(textFields: [UITextField], dismissable: Bool = true, previousNextable: Bool = false) {
for (index, textField) in textFields.enumerated() {
let toolbar: UIToolbar = UIToolbar()
toolbar.sizeToFit()
var items = [UIBarButtonItem]()
if previousNextable {
let previousButton = UIBarButtonItem(image: UIImage(named: "previous"), style: .plain, target: nil, action: nil)
previousButton.width = 30
if textField == textFields.first {
previousButton.isEnabled = false
} else {
previousButton.target = textFields[index - 1]
previousButton.action = #selector(UITextField.becomeFirstResponder)
}
let nextButton = UIBarButtonItem(image: UIImage(named: "next"), style: .plain, target: nil, action: nil)
nextButton.width = 30
if textField == textFields.last {
nextButton.isEnabled = false
} else {
nextButton.target = textFields[index + 1]
nextButton.action = #selector(UITextField.becomeFirstResponder)
}
items.append(contentsOf: [previousButton, nextButton])
}
let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: view, action: #selector(UIView.endEditing))
items.append(contentsOf: [spacer, doneButton])
toolbar.setItems(items, animated: false)
textField.inputAccessoryView = toolbar
}
}
}
And this is my alert controller where I am declaring the alerts and adding them:
alertDisplay.addTextField { (textField) in
textField.placeholder = "Enter Team 1 Score"
textField.keyboardType = UIKeyboardType.decimalPad
}
alertDisplay.addTextField { (textField) in
textField.placeholder = "Enter Team 2 Score"
textField.keyboardType = UIKeyboardType.decimalPad
}
To call the function I must add the textfields that I want in the array (which will allow the next and previous to go through textfields). This function is called within the viewDidLoad() method.
So the question is how can I make the two textfields within the alert to have this custom toolbar for the keyboard?
Below is my code. I have 9 buttons in my view whose background image is to be changed.
import UIKit
class ViewController: UIViewController {
let alerts = UIAlertController(title: "Hi", message: "Hello", preferredStyle: UIAlertControllerStyle.alert)
let action1 = UIAlertAction(title: "Yes", style: UIAlertActionStyle.default) { (<#UIAlertAction#>) in
for i in 0..<9
{
let newButton = self.view.viewWithTag(i) as! UIButton
newButton.setBackgroundImage(UIImage.init(named: ""), for: .normal)
}
}
#IBAction func buttonPressed(_ sender: UIButton)
{
sender.setBackgroundImage(UIImage.init(named: "cross.png"), for:.normal)
}
}
You need to add action to UIAlertController. Add following line after defining action1:
[alerts addAction: action1]
Try this:
first add button to the view and add action for it :
for i in 0..<9
{
let newButton = UIButton(frame : yourFrame)
newButton.setBackgroundImage(UIImage.init(named: ""), for: .normal)
newButton.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
newbutton.tag = i
self.view.addSubview(newButton:)
}
Try this :
newButton .setBackgroundImage(UIImage.init(named: ""), for: .normal)
newButton .setBackgroundImage(UIImage.init(named: "cross.png"), for: .selected)
#IBAction func buttonPressed(_ sender: UIButton)
{
sender.isSelected = !sender.isSelected;
}
I have two button namely programs and data.This two button have drop down option.But when i select the first drop down option for program and if i select any data from drop down .say like i select "super" from drop down .Then that super word are selecting in my both button.
With out selecting my second button drop down option.Its automatically changing.For clearly, if i select super data from drop down for program button,That word "super" is updating in my both drop down button.
Please help me to fix this problem :
Here is my code:
import UIKit
class UserDetailsController: UIViewController {
var addString: String!
let dropDown = DropDown()
#IBOutlet weak var programBtn: UIButton!
#IBOutlet weak var dataBtn: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
dropdownmethod()
}
func dropdownmethod () {
dropDown.dataSource = [
"Car",
"Motorcycle",
"Van",
"Truck",
"Bus",
"Bicycle",
"Feet",
"add"
]
dropDown.selectionAction = { [unowned self] (index, item) in
self.SpectifBtn.setTitle(item, forState: .Normal)
self.organBtn.setTitle(item, forState: .Normal)
if item == "add" {
if #available(iOS 8.0, *) {
let alert = UIAlertController(title: "Duplicate file", message: "Afile with the same name already exists.", preferredStyle:
UIAlertControllerStyle.Alert)
alert.addTextFieldWithConfigurationHandler(self.configurationTextField)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler:{ (UIAlertAction)in
print("User click Ok button")
print(self.textField.text)
self.addString = self.textField.text
self.dropDown.dataSource = [ "Car",
"Motorcycle",
"Van",
"Truck",
"Bus",
"Bicycle",
"Feet", "\(self.addString)", "add"]
self.SpectifBtn.setTitle(self.addString, forState: .Normal)
self.organBtn.setTitle(self.addString, forState: .Normal)
}))
self.presentViewController(alert, animated: true, completion: {
print("completion block")
})
} else {
// Fallback on earlier versions
}
}
}
dropDown.cancelAction = { [unowned self] in
self.dropDown.selectRowAtIndex(0)
self.SpectifBtn.setTitle("Canceled", forState: .Normal)
self.organBtn.setTitle("Canceled", forState: .Normal)
}
dropDown.anchorView = SpectifBtn
dropDown.bottomOffset = CGPoint(x: 0, y:SpectifBtn.bounds.height)
dropDown.dismissMode = .Automatic
dropDown.selectRowAtIndex(3)
}
func configurationTextField(textField: UITextField!)
{
if let aTextField = textField {
textField.text = "Filename"
self.textField = aTextField
}
}
#IBAction func programBtnPressed(sender: AnyObject) {
if dropDown.hidden {
dropDown.show()
} else {
dropDown.hide()
}
}
#IBAction func dataBtnPressed(sender: AnyObject) {
if dropDown.hidden {
dropDown.show()
} else {
dropDown.hide()
}
}
}
My screen shot.When i select any option from my program drop down.That option name is automatically updating in my data button name also
After dropdown selection of some item, you should check from what button it comes(by tag for example), now it seems you just select them both here:
self.SpectifBtn.setTitle(item, forState: .Normal) self.organBtn.setTitle(item, forState: .Normal)
So I have an view controller with a form like so:
When I use the back and forward buttons on the input accessory view, everything works fine. However, in simulator when I use the tab key on the keyboard to traverse the text fields, and then segue back to the previous view controller, I get the following:
and then this in the console:
I look for zombie objects in Instruments and find this:
but that doesn't provide much help. Any idea why this would ONLY happen when tab is pressed on the keyboard in simulator? It isn't an issue on the device and I can't reproduce it there but I feel like a zombie object needs to be address either way.
and here is my code for that view controller:
in viewDidLoad i'm calling setupInputAccessoryView
func setupInputAccessoryView(){
// Create a button bar for the number pad
let keyboardDoneButtonView = UIToolbar()
keyboardDoneButtonView.sizeToFit()
// Setup the buttons to be put in the toolbar.
let item4 = UIBarButtonItem(title: "Done", style: UIBarButtonItemStyle.Plain, target: self, action: Selector("endEditingNow") )
let item = UIBarButtonItem(title: " < ", style: UIBarButtonItemStyle.Plain, target: self, action: Selector("previousField"))
let item2 = UIBarButtonItem(title: " >", style: UIBarButtonItemStyle.Plain, target: self, action: Selector("nextField"))
let item3 = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.FlexibleSpace, target: self, action: nil)
if let font = UIFont(name: "Helvetica", size: 17) {
item4.setTitleTextAttributes([NSFontAttributeName: font], forState: UIControlState.Normal)
}
if let font2 = UIFont(name: "Helvetica", size: 26){
item.setTitleTextAttributes([NSFontAttributeName: font2], forState: UIControlState.Normal)
item2.setTitleTextAttributes([NSFontAttributeName: font2], forState: UIControlState.Normal)
}
//Add buttons to toolbar and add as an inputAccessoryView
var toolbarButtons = [item, item2, item3, item4]
keyboardDoneButtonView.setItems(toolbarButtons, animated: false)
keyBoardToolBar = keyboardDoneButtonView
}
and then in textFieldDidBeginEditing I have:
func textFieldDidBeginEditing(textField: UITextField) {
currentTextField = textField
textField.inputAccessoryView = keyBoardToolBar
if (textField == stateField){
statePicker.frame = CGRectZero
statePicker.delegate = self
statePicker.dataSource = self
textField.inputView = statePicker
}else if (textField == countryField){
countryPicker.frame = CGRectZero
countryPicker.delegate = self
countryPicker.dataSource = self
textField.inputView = countryPicker
}
UIView.animateWithDuration(0.5, animations: { () -> Void in
self.scrollView.contentOffset.y = textField.center.y - 30
})
}
and then my selectors for the toolbar buttons
func endEditingNow(){
self.view.endEditing(true)
}
func nextField(){
var nextTag = currentTextField!.tag + 1
self.view.viewWithTag(nextTag)?.becomeFirstResponder()
}
func previousField(){
var nextTag = currentTextField!.tag - 1
self.view.viewWithTag(nextTag)?.becomeFirstResponder()
}