UIAlertController utility class fails to call UITextField delegate or target but works in UIViewController

我怕爱的太早我们不能终老 提交于 2019-12-11 04:23:57

问题


I encountered a strange issue and maybe it is only a lack of knowledge about Swift 3.0 / iOS 10 so hopefully you can guide me into the right direction or explain to me what I'm doing wrong.

HOW IT CURRENTLY WORKS

I am trying to create a UIAlertController with style .alert so I can get a user text input for my app. My requirements are that the text must not be empty and if there was a text there before, it must be different.

I can use the following code to achieve what I want:

//This function gets called by a UIAlertController of style .actionSheet
func renameDevice(action: UIAlertAction) {

    //The AlertController
    let alertController = UIAlertController(title: "Enter Name",
                                            message: "Please enter the new name for this device.",
                                            preferredStyle: .alert)

    //The cancel button
    let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)

    //The confirm button. Make sure to deactivate on first start
    let confirmAction = UIAlertAction(title: "Ok", style: .default, handler: { action in
        self.renameDevice(newName: alertController.textFields?.first?.text)
    })

    //Configure the user input UITextField
    alertController.addTextField { textField in
        log.debug("Setting up AlertDialog target")
        textField.placeholder = "Enter Name"
        textField.text = self.device.getName()
        textField.addTarget(self, action: #selector(self.textFieldDidChange(_:)), for: .editingChanged)
    }
    //Disable the OK button so that the user first has to change the text
    confirmAction.isEnabled = false
    self.confirmAction = confirmAction
    //Add the actions to the AlertController
    alertController.addAction(cancelAction)
    alertController.addAction(confirmAction)
    present(alertController, animated: true, completion: nil)

}
var confirmAction: UIAlertAction?

func textFieldDidChange(_ textField: UITextField){
    log.debug("IT CHAGNED!=!=!=!=!")
    if let text = textField.text {
        if !text.isEmpty && text != self.device.getName() {
            confirmAction?.isEnabled = true
            return
        }
    }
    confirmAction?.isEnabled = false
}

//Finally this code gets executed if the OK button was pressed
func renameDevice(newName: String?){ ... }

HOW I WANT IT TO WORK

So far so good but I'm going to ask the user for a text input at various places so I want to use a utility class to handle all this stuff for me. The final call shall look like this:

func renameDevice(action: UIAlertAction) {
     MyPopUp().presentTextDialog(title: "Enter Name",
                                 message: "Please enter the new name for this device.",
                                 placeholder: "New Name",
                                 previousText: self.device.getName(),
                                 confirmButton: "Rename",
                                 cancelButton: "Cancel",
                                 viewController: self){ input: String in
        //do something with the input, e. g. call self.renameDevice(newName: input)

    }

WHAT I CAME UP WITH

So I implemented everything in this little class:

class MyPopUp: NSObject {

var confirmAction: UIAlertAction!
var previousText: String?
var textField: UITextField?

func presentTextDialog(title: String, message: String?, placeholder: String?, previousText: String?, confirmButton: String, cancelButton: String, viewController: UIViewController, handler: ((String?) -> Swift.Void)? = nil) {

    //The AlertController
    let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)

    //The cancel button
    let cancelAction = UIAlertAction(title: cancelButton, style: .cancel)

    //The confirm button. Make sure to deactivate on first start
    confirmAction = UIAlertAction(title: confirmButton, style: .default, handler: { action in
        handler?(alertController.textFields?.first?.text)
    })

    //Configure the user input UITextField
    alertController.addTextField { textField in
        log.debug("Setting up AlertDialog target")
        self.textField = textField
    }

    //Set placeholder if necessary
    if let placeholder = placeholder {
        self.textField?.placeholder = placeholder
    }

    //Set original text if necessary
    if let previousText = previousText {
        self.textField?.text = previousText
    }

    //Set the target for our textfield
    self.textField?.addTarget(self, action: #selector(textChanged), for: .editingChanged)

    log.debug("It appears that our textfield \(self.textField) has targets: \(self.textField?.allTargets)")

    //Store the original text for a later comparison with the new entered text
    self.previousText = previousText

    //Disable the OK button so that the user first has to change the text
    confirmAction.isEnabled = false

    //Add the actions to the AlertController
    alertController.addAction(cancelAction)
    alertController.addAction(confirmAction)
    viewController.present(alertController, animated: true, completion: nil)
}

func textChanged() {
    if let text = textField?.text {
        if !text.isEmpty && text != previousText {
            confirmAction.isEnabled = true
            return
        }
    }
    confirmAction.isEnabled = false
}
}

THE PROBLEM

My problem is that no matter where I try to set the target for the UITextField of the UIAlertController, it never executes my target. I tried setting the TextFields delegate in alertController.addTextField{} as well as setting the target there. The issue which confuses me the most is that setting the placeholder and original text works just fine but delegate or target functions are never called. Why does the same code works when executed in a UIViewController but does not work when executed in a utility class?

THE SOLUTION (UPDATE)

Apparently I made a mistake. In my viewcontroller, I create an instance of MyPopUp and call the present() function on it.

MyPopUp().presentTextDialog(title: "Enter Name",
                             message: "Please enter the new name for this device.",
                             placeholder: "New Name",
                             previousText: self.device.getName(),
                             confirmButton: "Rename",
                             cancelButton: "Cancel",
                             viewController: self)

In the presentTextDialog() I thought setting the current instance of MyPopUp as the delegate/target would be enough but it seems that the MyPopUp instance is released immediately and therefore never called. My very simple workaround is to create the MyPopUp instance in an instance variable and call the present method whenever I need to.

let popup = MyPopUp()
func renameDevice(action: UIAlertAction) {
    popup.presentTextDialog(...){ userinput in
        renameDevice(newName: userinput)
    }
}

回答1:


Okay so here's exactly what I did wrong.

  1. I created a utility class which I had to instantiate

  2. The class itself was basically empty and it's only purpose was to be the target or delegate of the UITextField

  3. I instantiated the class and immediately called the presentation function without keeping a reference around

By not keeping a reference to my instance, the object must have gotten released immediately after presenting the UIAlertController in my viewcontroller.

Solution: Just keep a reference around in your viewcontroller. Of course a local variable won't do. I store the reference in an instance variable of my viewcontroller but this doesn't feel very "swifty". I'm still a beginner in swift and maybe my mind is "damaged" by other languages (java, c#). I will probably start by making my utility class a singleton or creating an extension for UIViewController to present the alert. If you have other good ideas feel free to teach them to me :)




回答2:


Instead of presenting dialogue in NSObject class. You must use delegates and protocol to present an alert. Replace your code with this. In View Controller we have a function named renameDevice. You can present alert here.

MyPopUp.swift

import UIKit

class MyPopUp: NSObject {
    var confirmAction: UIAlertAction!
    var previousText: String?
    var textField: UITextField?
    var delegate : MyPopUpDelegate!


    func textChanged() {
        if let text = textField?.text {
            if !text.isEmpty && text != previousText {
                confirmAction.isEnabled = true
                return
            }
        }
        confirmAction.isEnabled = false
    }
}
protocol MyPopUpDelegate {
    func renameDevice(action: UIAlertAction)
}

ViewController.swift

import UIKit

class ViewController: UIViewController,MyPopUpDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.



    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    func renameDevice(action: UIAlertAction) {

    // Add dialogue that you wnat to create.

    }

}



回答3:


In your MyPopUp class first you need to conform to UITextFieldDelegate method like this

class MyPopUp:NSObject,UITextFieldDelegate {

then while adding your UITextField to alert you need to set delegate to that UITextField like this

alertController.addTextField { textField in
    log.debug("Setting up AlertDialog target")
    textField.delegate = self
    self.textField = textField
}

then you need to implement UITextField Delegate method to get the change in your UITextField like this

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

This will solve your problem.Also check this



来源:https://stackoverflow.com/questions/40837477/uialertcontroller-utility-class-fails-to-call-uitextfield-delegate-or-target-but

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!