2

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)
    }
}
xxtesaxx
  • 6,175
  • 2
  • 31
  • 50

3 Answers3

2

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 :)

xxtesaxx
  • 6,175
  • 2
  • 31
  • 50
0

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.

    }

}
User511
  • 1,456
  • 10
  • 28
  • Presenting the UIAlertController is not the issue since I put a reference to the viewcontroller in the function call and then execute viewController.present(alertController, animated: true) in the last line of presentTextDialog. The issue is that the UITextField never calls my delegate/target if I do it via my popup class. Thats the question: why does the exact same code works when used in a view controller but does not work when used in a utility class? Am I missing some special objective c stuff like a certain protocol or something? – xxtesaxx Nov 28 '16 at 05:37
  • Where are you calling the delegate. I cant see that – User511 Nov 28 '16 at 05:38
  • In the example code I not use a delegate but i use textField.addTarget() instead – xxtesaxx Nov 28 '16 at 05:39
  • I tried both, adding a target to the textfield as well as setting its delegate. both ways don't work – xxtesaxx Nov 28 '16 at 05:40
  • Ok and where are you calling renameDevice function? – User511 Nov 28 '16 at 06:00
  • Make sure if you need the AlertController you must have to add it to the hierarchy first as MyPopUp is not in the hierarchy so you cannot present directly from MyPopU view. – User511 Nov 28 '16 at 06:20
  • Look, in my view controller, I create a new instance of my popup and then call present() on the popup. The present() function takes an UIViewController argument so I call: myPopup.present(self). In the present method, I call "viewController.present(...)" which means that my viewcontroller is the one who presents the alertcontroller – xxtesaxx Nov 28 '16 at 07:43
  • Yes I can see but if you will check with debuting you will see your view controller has no memory. You cannot present directly if the view is not in hierarchy. So you must have to use protocols and delegates. Thats why I have added this answer. You can use text field delegates in same way. – User511 Nov 28 '16 at 09:16
  • I don't know what you mean but while I was thinking about what you could mean, the actual solution came to my mind. My problem lies in that the instance of MyPopUp is not retained when I call present(..) on it. So at the end of the method it is released. Apparently its not enough to set the MyPopUp instance as the textfield delegate in present(..) to retain it. I'll update my question, then you'll see what I did wrong. Thanks for making me think in another way :) – xxtesaxx Nov 28 '16 at 11:44
0

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

Community
  • 1
  • 1
Rajat
  • 10,977
  • 3
  • 38
  • 55
  • Well that is exactly what I did. As I mentioned, I tried both approaches. I tried the delegate approach AND the addTarget approach and neither of them work... – xxtesaxx Nov 28 '16 at 07:39
  • The answer you suggested does more or less the same as my working example. However, i want to put this in a utility class so that I *don't* have to implement the delegate pattern everytime in each viewcontroller – xxtesaxx Nov 28 '16 at 07:42