Swift UIAlertController file rename action - ios

I am using Swift 2. This question relates to iOS9.
In brief:-
If the file rename button is clicked and the file name is still invalid then do I need to present the alert again or is there a smarter way of handling this?
In full:-
Saving a file imported from iCloud, I am presenting a UIAlertController called alertController if a file with the same name (.lastPathComponent) already exists in /Documents.
The UIAlertController has two actions titled Cancel and Rename and a .addTextFieldWithConfigurationHandler. If the file name already exists then the user is prompted to either cancel or rename.
This question relates to validating the new name and re-presenting (or not dismissing) the UIAlertController until the file name is valid:
If the user clicks the rename button and the file name is still the same, or is the same as another file which already exists, then I want the UIAlertController to be shown again. Or better still it would not be dismissed until the file name is valid.
The way I have done this (the only way I have figured out) is by adding a func named presentAlertController which presents the UIAlertController. This func is called from the rename handler when the button is clicked if the file name already exists (or has not been changed). (The same as simply presenting the UIAlertController again from the action).
My question:-
My code does what I want but can anyone suggest a neater and less clumsy way of achieving this outcome - without having to present the UIAlertController again?
Here is the relevant code. Please note that this whole section is within the completion handler of another function - hence the need for various references to self and why the UIAlertController code is within a dispatch_async(dispatch_get_main_queue()) (has to be called from the main queue).
//...
if checkFileExists(saveURL) { // saveURL: NSURL
dispatch_async(dispatch_get_main_queue()) {
let message = "File named `\(saveURL.lastPathComponent!)` already exists. Please import using a new name or else cancel."
let alertController = UIAlertController(title: "", message: message, preferredStyle: UIAlertControllerStyle.Alert)
alertController.addTextFieldWithConfigurationHandler { textField -> Void in
textField.text = saveURL.lastPathComponent! // it presents a text field containing the file name which needs to be changed
}
func presentAlertController() {
self.presentViewController(alertController, animated: true) {}
}
alertController.addAction(UIAlertAction(title: "Cancel", style: .Cancel, handler: nil))
alertController.addAction(UIAlertAction(title: "Rename", style: .Default) { action -> Void in
saveURL = saveURL.URLByDeletingLastPathComponent!.URLByAppendingPathComponent((alertController.textFields?.first!.text)!)
if checkFileExists(saveURL) {
presentAlertController() // currently it is calling a function to present the UIAlertController again if the file still exists when the button is clicked
} else {
saveXML(saveURL, dataObject: self.myThing)
self.fileTableView.reloadData()
}
})
presentAlertController() // this will be the first time that this called
}
}
//...

So you can add a target to your text field that will be called when the text field is edited, and in that function you can check if the user has typed a valid name. The problem you'll have with that is you'll need access to alertController so you can disable the "Rename" button. You can accomplish that by making a property at the top of your view controller like so:
var alertController: UIAlertController!
then revise the code your posted like so:
//...
if checkFileExists(saveURL) { // saveURL: NSURL
dispatch_async(dispatch_get_main_queue()) {
let message = "File named `\(saveURL.lastPathComponent!)` already exists. Please import using a new name or else cancel."
//just assigning here, not redeclaring (get rid of "let")
alertController = UIAlertController(title: "", message: message, preferredStyle: UIAlertControllerStyle.Alert)
alertController.addTextFieldWithConfigurationHandler { textField -> Void in
textField.text = saveURL.lastPathComponent! // it presents a text field containing the file name which needs to be changed
//Add the target here, calls on checkString
textField.addTarget(self, action: "checkString:", forControlEvents: UIControlEvents.EditingChanged)
}
func presentAlertController() {
self.presentViewController(alertController, animated: true) {}
}
alertController.addAction(UIAlertAction(title: "Cancel", style: .Cancel, handler: nil))
alertController.addAction(UIAlertAction(title: "Rename", style: .Default) { action -> Void in
saveURL = saveURL.URLByDeletingLastPathComponent!.URLByAppendingPathComponent((alertController.textFields?.first!.text)!)
if checkFileExists(saveURL) {
presentAlertController() // currently it is calling a function to present the UIAlertController again if the file still exists when the button is clicked
} else {
saveXML(saveURL, dataObject: self.myThing)
self.fileTableView.reloadData()
}
})
//Disable the "Rename" button to start, remove this line if you don't want that to happen
(alertController.actions as! [UIAlertAction])[1].enabled = false
presentAlertController() // this will be the first time that this called
}
}
//...
Then you'll have to make a checkString function (obviously you can rename this however you like as long as you also change the selector on the line where you add the target to the text field). Here's a little bit of code to give you an idea, but you'll have to write your own stuff here.
func checkString(sender: UITextField) {
//Pretty safe assumption you don't want an empty string as a name
if sender.text == "" {
(alertController.actions as! [UIAlertAction])[1].enabled = false
}
//As soon as the user types something valid, the "Rename" button gets enabled
else {
(alertController.actions as! [UIAlertAction])[1].enabled = true
}
}
This code has been tested, but not very rigorously, so comment if you have problems or if it works.

Related

Dropbox delegate methods are not calling first time

This is i have written in viewDidLoad.
if DBSession.shared().isLinked() {
print("already linked")
initDropboxRestClient()
}
else
{
print("connecting2")
DBSession.shared().link(from: self)
initDropboxRestClient()
}
and function initDropboxRestClient() is written below.
func initDropboxRestClient() {
dbRestClient = DBRestClient(session: DBSession.shared())
dbRestClient.delegate = self
dbRestClient.loadMetadata("/")
}
The problem is i have two view controllers for displaying dropbox file names, the first view controller is calling the delegate methods and displaying filename and folder names perfectly. But the second one isn't.
In the second view controller,
I observed that if i scroll my tableview in second view controller up and down then the delegate methods get called immediately and once it is linked next time the methods are called immediately.
So for the first time delegate methods are not getting called in my second dropbox view controller thats my problem here. Thanks in advance.
The restClient delegate methods are as follows.
func restClient(_ client: DBRestClient!, loadedMetadata metadata: DBMetadata!) {
for file in metadata.contents
{
dbMetadataArray.append(file as! DBMetadata)
fileNamesArray.append((file as AnyObject).filename)
}
tableView.reloadData()
self.myActivityIndicator.stopAnimating()
self.myActivityIndicator.hidesWhenStopped = true
}
func restClient(_ client: DBRestClient!, loadMetadataFailedWithError error: Error!) {
print("in loadMetadataFailedWithError method in dropbox email view controller")
print("Error dscription = %#",[error.localizedDescription])
let alert = UIAlertController(title: "Go Back.", message: "Try Once Again", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil))
self.myActivityIndicator.stopAnimating()
self.myActivityIndicator.hidesWhenStopped = true
tableView.reloadData()
}
There are a few things that might cause your delegate methods to not be called:
Your rest client is nil or is being released (e.g., by ARC) prematurely.
You're making the call in a background thread that doesn't have a run loop.
Your delegate method that should be called back has a typo in it. Unfortunately the SDK doesn't warn you if it can't find a delegate method to call; it just completes without telling anyone.
Also, note that the SDK you're using uses API v1, which is deprecated and being retired soon anyway:
https://blogs.dropbox.com/developers/2016/06/api-v1-deprecated/
You should switch to API v2:
https://www.dropbox.com/developers/documentation

Having trouble 'segueing' from one view controller to another, getting weird warnings

I am getting a very strange error. I think the compiler is trying to tell me that it can't segue to another view controller until it is done executing all the code in my current view controller but I am not sure.
I'm literally getting input by using an alert box (i.e. calling a function called generateTextField).
Then when I am done I'm saying "Hey I want you to go to another view controller" - but the the compiler instead tells me "Hey I don't think so".
Here is my error:
Warning: Attempt to present HairStyle1ViewController: 0x7...> on browseBarbersViewController: 0x7...> which is already presenting
Warning: Attempt to present HairStyle1ViewController: 0x7..> on browseBarbersViewController: 0x7...> which is already presenting
#IBAction func AddNewStyleButtonClicked(sender: AnyObject)
{
// Get the "hairstyle name" from the user
generateTextField();
// OK We are done with that function, now transition to the
// next screen
performSegueWithIdentifier("HairStyle1", sender: self);
}
// Generate a text field for user input (i.e. call the alert function)
func generateTextField()
{
//1. Create the alert controller.
var tempStyle = "";
var alert = UIAlertController(title: "Add a New Style", message: "Enter the name of the new hairstyle below", preferredStyle: .Alert);
//2. Add the text field. You can configure it however you need.
alert.addTextFieldWithConfigurationHandler({ (textField) -> Void in
textField.placeholder = "Your New Hairstyle Goes Here..";
})
//3. Grab the value from the text field, and print it when the user clicks OK.
alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: { (action) -> Void in
let textField = alert.textFields![0] as UITextField
tempStyle = textField.text!;
print("New Style Added is: " + tempStyle);
HairStyle = tempStyle;
}))
// 4. Present the alert.
self.presentViewController(alert, animated: true, completion: nil)
}
It's also weird that when I take out the generateTextField() function it performs the segue perfectly. I'm very confused.
Wow, I figured it out. I had to instead, segue in the body of the alert function.
I fixed this by adding
self.performSegueWithIdentifier("HairStyle1", sender: self);
after the
HairStyle = tempStyle; line

iOS: Which function to override when I need to do something after the view is visible to the user?

So the first thing my app does is get a list of movies from an API. I'm trying to handle if there's a network issue. Currently, in my viewDidLoad method, I call "updateApiInfo", which contains the following code:
if self.movies == nil {
print("stuff went wrong")
let alert = UIAlertController(title: "Network Error", message: "There was a nework error.\nYou must be connected to the internet to use Flicks.", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Exit", style: UIAlertActionStyle.Default, handler: {(UIAlertAction) in
UIControl().sendAction(Selector("suspend"), to: UIApplication.sharedApplication(), forEvent: nil)
}))
self.presentViewController(alert, animated: true, completion: nil)
}
When viewDidLoad calls updateApiInfo, I get this message:
Warning: Attempt to present <UIAlertController: 0x7fad10e3ad80> on <Flicks.MoviesViewController: 0x7fad10e35cc0> whose view is not in the window hierarchy!
When I call updateApiInfo just from the user refreshing, the error pops up as expected.
I'm assuming that viewDidLoad only gets called before the view gets displayed to the user, which I guess is causing this error. Is there a method I can stick this code in for after the view is displayed to the user, so the problem can presumably get fixed?
You need to use viewDidAppear: to make sure the view is already in the window hierarchy.
There's a pretty good explanation of the order in which the methods are called here:
What is the process of a UIViewController birth (which method follows which)?

Change title of button

I have an alertController with a button in it. Here's how I created the button:
var myButton : UIAlertAction!
// Later...
self.myButton = UIAlertAction(title: "myButton", style: .Default, handler: {action -> Void in
self.myButton.title = "✔︎\(myButton)"
})
After the button get's selected, I want to add a "✔︎" at the beginning of the title. (As you can see in the last line of code above.) When I try doing that, I get the following error:
Cannot assign to the result of this expression
What can I do to fix that? If myButtons title is read-only, then is there a valid workaround to it?
The error is not about the "✔︎". It's because of myButton. It means nothing to assign a UIAlertAction to String. That's how you get the error.

Loop user input from text boxes in Swift

I have two text boxes in my iOS to-do app, one for the name of the task and the other for the description. When the user leaves one or both of the text boxes blank, I want to alert the user about the problem and loop this until the two text boxes are no longer blank. Here is the code I have:
var validInput :Bool = false //for while
while (validInput == false) {
if (txtTask.text == "" || txtDesc.text == "") {
var alert = UIAlertController(title: "Error", message: "Task and description cannot be blank", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Working!!", style: UIAlertActionStyle.Default, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
} else {
validInput == true
}
}
This code is inside an #IBAction function which runs when the user presses Done. My code runs in an infinite loop and it is pretty obvious why. How can I achieve what I would like?
I have an idea:
User leaves the text boxes blank and presses done.
An alert pops up warning the user.
Skip the rest of the function, and only run the function again when Done is pressed.
How could I a) put the above into code, or b) use a loop like I have above, properly?
The answer is: Don't do this !
You don't need to loop textFields to watch for value changes. The correct way to do this is using using the UITextField's delegate methods like
- textFieldDidBeginEditing: to know when user did begin editing,
- textField:shouldChangeCharactersInRange:replacementString: when the textField text value changes
- textFieldDidEndEditing: to know when the user end editing
etc...
As described on the docs:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITextFieldDelegate_Protocol/
Using loops to do this kind of thing is a bad practice in this case. (And you will have to do a lot of stuff to not block the current thread, verify if there is already an Alert on the screen etc)
If you use a while loop in your "done" button, it'll be stuck in an infinite loop as you said. The user gets no chance to change anything because of this.
Instead you should use an if statement to check if those boxes are empty and give them a warning if it is, and do nothing.
if condition {
// Execute your code if both boxes are filled
} else {
// Show alert
}
If you insists on using a while loop, you'll have to let the user to enter the text in your alerts. Then your code would work.
Here no need of while loop. Just do it in a if else loop, because each time you press the Done button after filing text or leaving empty, your piece of code will be executed.
if (txtTask.text == "" || txtDesc.text == "") {
var alert = UIAlertController(title: "Error", message: "Task and description cannot be blank", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Working!!", style: UIAlertActionStyle.Default, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
} else {
//Do something or print.
}
I assume this piece of code is in #IBAction method.
Keep it simple:
#IBOutlet var textA: UITextField!
#IBOutlet var textB: UITextField!
#IBAction func validateButton(sender: AnyObject) {
if (textA.text == "" || textB.text == "") {
println("ALERT: BLANK FIELDS")
} else {
println("Let's run some code since we're not blank")
}
}

Resources