I am trying to create an alert controller which has a custom view inside it. The custom view is loaded from xib file. The custom view contains a uiswitch. The problem is that its click event is not being triggered and from UI switch is not turned on/off on click. Here is the code I am trying to work on:
This is the button click event of my view controller to present alert:
#IBAction func btnClicked(_ sender: UIButton) {
let alertController = UIAlertController(title: "\n\n\n\n\n\n", message: "", preferredStyle: UIAlertControllerStyle.alert)
let customView=Bundle.main.loadNibNamed("CustomView", owner: self, options: nil)!.first! as! CustomView
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: {(alert: UIAlertAction!) in print("cancel")})
alertController.addAction(cancelAction)
let somethingAction = UIAlertAction(title: "Something", style: .default, handler: {(alert: UIAlertAction!) in print("something")})
alertController.addAction(somethingAction)
alertController.view.addSubview(customView)
self.present(alertController, animated: true, completion:{})
}
This is the code inside CustomView class:
class CustomView: UIView {
#IBAction func switchClicked(_ sender: UISwitch) {
print("switch cliked")
}
}
My CustomView.xib file has the referencing layout properly set. It just has one uiswitch. The CustomView.xib has its size set to 'freeform'(not sure if it matters). I also tried setting isUserInteractionEnabled to false or true at various places for CustomView and/or alertController.view after searching for similar issues but nothing works.
You should be capturing the valueChanged control event but if you want to do the value change programmatically via click I suppose you could do:
class CustomView: UIView {
#IBAction func switchClicked(_ sender: UISwitch) {
sender.setOn(!sender.isOn, animated: true)
}
}
I think xib view will triger method only when same class has been loaded in memory. In your case this might be an issue . To test this u can add custom view in same class in which u are showing alert .
Related
I've created UIAlertController with Three action buttons, now I want to set text to a TextView on each button.
Problem is this that when I click a button then result of previously clicked button is printed on TextView.
My Code
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var mTextView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
mTextView.text = ""
// Do any additional setup after loading the view, typically from a nib.
}
#IBAction func showDialogOnClick(_ sender: UIButton) {
//Creating UIAlert, giving its title and message, setting style(alert OR alertSheet)
let mDialog = UIAlertController(title: "Title", message: "This is the message" , preferredStyle: .actionSheet)
//add actions/buttons to alert
mDialog.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: {(action) in
print("Cancel")
self.mTextView.text = "Cancel"
}))
mDialog.addAction(UIAlertAction(title: "Default", style: .default, handler: {(action) in
print("Default")
self.mTextView.text = "Default"
}))
mDialog.addAction(UIAlertAction(title: "Destructive", style: .destructive, handler: {(action) in
print("Destructive")
self.mTextView.text = "Destructive"
}))
self.present(mDialog, animated: true)
}
}
What i think can be possible issue is you are testing this in your Simulator and your computer is having low graphic configuration same thing happens with be when i test my code in my old mac mini.You can do two things :-
Test the code in a physical device
Try to minimize your app in your simulator and come back to the app you'll see the effect Or change the view back and forth to see the effect.
Let me know if I was right.
#IBOutlet weak var myPictureOutlet: UIButton!
#IBAction func myPictureAction(_ sender: Any) {
let alert = UIAlertController(title: "Chnage Pic", message: "to Change the pic, press the button below", preferredStyle: . alert)
alert.addAction(UIAlertAction(title: "change picture", style: .default, handler: { (action) in
alert.dismiss(animated: true, completion: nil)
self.myPictureOutlet.setBackgroundImage(ButtonImage, for: UIControlState.normal)
}))
self.present(alert, animated: true, completion: nil)
}
My goal is to:
create an UIAlertController when a UIButton (with a default picture) is clicked
then, when a user clicks on the button in the UIAlertController, it changes the UIButton picture.
I am having a little trouble with this task. The UIAlert is working when I touch the UIButton, however the UIButton background is not changing. Please review my code and tell me if something is wrong.
Your code should actually work. You don't need this line though:
alert.dismiss(animated: true, completion: nil)
UIAlertController will remove it for you after you press the alert button.
Also to improve your code just refer to the IBAction sender like this:
guard let button = sender as? UIButton else {
print("Ooops, not a button!")
return
}
button.setBackgroundImage(ButtonImage, for: UIControlState.normal)
I have a childViewController which is pushed from parentViewController. In childViewController I want to block pop action in a particular condition.
I wrote this code in viewWillDisappear: But I guess need to do this somewhere else.
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if changesMade {
let alertController = UIAlertController(title: "Alert", message: "Changes made are not saved. Do you wish to save changes made?", preferredStyle: .alert)
let cancelOption = UIAlertAction(title: "Cancel", style: .cancel)
let saveOption = UIAlertAction(title: "Save", style: .default, handler: { (action) in
self.saveSession()
})
alertController.addAction(saveOption)
alertController.addAction(cancelOption)
present(alertController, animated: true)
}
}
Actually, there is a very simple solution: navigationItem.hidesBackButton = true - this will hide "BACK" button and disable swipe-to-back feature. 🤓
There are two cases here:
User can pop using back button.
User can pop using interactive pop gesture of navigation controller.
I think you should use a custom back button and name it Done and write your logic of showing alert on press of this button.
Using the custom back button would disable the interactive pop gesture by default and you will be spared from playing the dance of enabling/disabling interactivePopGesture on navigation controller.
block pop action till your changes are not saved like this
if changesMade {
let alertController = UIAlertController(title: "Alert", message: "Changes made are not saved. Do you wish to save changes made?", preferredStyle: .alert)
let cancelOption = UIAlertAction(title: "Cancel", style: .cancel)
let saveOption = UIAlertAction(title: "Save", style: .default, handler: { (action) in
self.saveSession()
self.navigationController?.popViewController(animated: true)
})
alertController.addAction(saveOption)
alertController.addAction(cancelOption)
present(alertController, animated: true)
}
Update - Add this below custom button and its action in child View controller which is being pushed from parent View Controller
so, without satisfying your condition user can not move from child to parent again
For customising action of navigation backButton you need to manually add a back Button using below line , you can Customise barButton being added here in DidLoad
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Back", style: UIBarButtonItemStyle.plain, target: self, action: #selector(self.backToInitial(sender:)))
It will perform required Action
#objc func backToInitial(sender: AnyObject) {
if changesMade {
let alertController = UIAlertController(title: "Alert", message: "Changes made are not saved. Do you wish to save changes made?", preferredStyle: .alert)
let cancelOption = UIAlertAction(title: "Cancel", style: .cancel)
let saveOption = UIAlertAction(title: "Save", style: .default, handler: { (action) in
self.saveSession()
self.navigationController?.popViewController(animated: true)
})
alertController.addAction(saveOption)
alertController.addAction(cancelOption)
present(alertController, animated: true)
}
}
and I do not think you can stop default back button action task of navigation Controller as it is designed the way to perform it
But yes you can manage it in ViewWillDisappear as :
override func viewWillDisappear(_ animated: Bool) {
if self.isMovingFromParentViewController || self.isBeingDismissed {
self.navigationController?.popViewController(animated: false) //here task but will not result as Expected output
}
}
-----------------Re-Update ---------------
in swift I used a objective-C class to get output as expected now, childViewController pop action is being controller from a alert using default back button that we get from navigation controller
You can customise you pop action to perform or not until your condition is not satisfied
Github Link - https://github.com/RockinGarg/NavigationBackButton.git
I found a much more elegant solution (in my opinion).
It works no matter how the user triggers the pop (accessibility escape, swipe back gesture, tapping back) since it overrides the built in pop methods that the system uses.
Swift 5
public class DiscardSafeNavigationController:UINavigationController {
/// Should the pop be prevented? Set this to `true` when you have changes which need to be protected
public var hasUnsavedChanges:Bool = false
/// Show a prompt on the top most screen asking the user if they wish to proceed with the pop
/// - Parameter discardCallback: The callback to use if the user opts to discard
private func confirmDiscardChanges(discardCallback:#escaping (()->())) {
let alertController = UIAlertController.init(title: "Discard changes", message: "Are you sure you want to discard any unsaved changes?", preferredStyle: UIAlertController.Style.alert)
alertController.addAction(UIAlertAction.init(title: "Discard", style: UIAlertAction.Style.destructive, handler: { (_) in
discardCallback()
//User elected to discard and so, at this point, they no longer have changes to save
self.hasUnsavedChanges = false
}))
alertController.addAction(UIAlertAction.init(title: "Cancel", style: UIAlertAction.Style.cancel, handler: nil))
self.topViewController?.present(alertController, animated: true, completion: nil)
}
override func popViewController(animated: Bool) -> UIViewController? {
//If there aren't unsaved changes, popping is safe
if !hasUnsavedChanges {
return super.popViewController(animated: animated)
}else {
//Changes have been made. Block the pop and first check with the user before continuing
confirmDiscardChanges {
super.popViewController(animated: animated)
}
return nil
}
}
}
and when you want to enable discard protection from any child view controllers, simply use:
(self.navigationController as? DiscardSafeNavigationController)?.hasUnsavedChanges = true
and then any time the navigation controller is asked to pop it, the navigation controller will ask the user first.
I would like to prevent the UIAlertController from dismissing.
I have a UIAlertAction that simply appends a string into the UIAlertTextField, however, once tapped it dismisses the view controller [undesired]. I've tried adding an NSNotification with undesired results.
UIAlertAction *pasteMessage = [UIAlertAction actionWithTitle:#"Paste Message" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
UITextField *textField = alertC.textFields.firstObject;
textField.text = [textField.text stringByAppendingString:[NSString stringWithFormat:#"%#", copiedString]];
}];
I've also tried setting no to pasteMessage by:
[alertC canPerformAction:#selector(dismissViewControllerAnimated:completion:) withSender:pasteMessage];
-(void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
UIAlertController *alertController = (UIAlertController *)self.presentedViewController;
UIAlertAction *paste = alertController.actions.firstObject;
if (paste) {
flag = NO;
} else {
flag = YES;
}
}
Edit, i'm not looking to prevent the tapping of UIAlertAction i'm looking to prevent the UIAlertController from dismissing when tapping on said action. The action can be enabled/disabled whatever, but my goal is to simply paste the copied message into the UITextField by pressing an action (hence the reason I don't want it to be dismissed)
I also realize setting the BOOL to dismissViewControllerAnimated: simply sets it to not animate the view controllers dismissal, I don't want it to imply it was for stopping the actual dismissal process. Simply offering the things I've tried in relation to my goal. I've also tried presenting a new UIAlertController when selecting pasteMessage auto-populating the new UIAlertControllers textField with the copied message, it works, but I feel like it's too hacky for what could be done.
EDIT: Updated for Swift 5
EDIT: Updated to include #skywalker's feedback
So I actually got this to work. In short, it involves adding a long-press gesture recognizer to the UIAlertController that triggers before the dismissal occurs.
First, create lazily loaded computed variables in your view controller for your UIAlertController and the UIAlertAction you want to prevent from triggering so that self is accessible via the gesture recognizer's selector method you'll be attaching to the alert (self in the selector insinuates that all of this is inside a view controller).
lazy var alert: UIAlertController = {
let alert = UIAlertController(title: "Title", message: "Message", preferredStyle: .alert)
alert.addTextField(configurationHandler: nil)
let appendAction = self.appendAction
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alert.addAction(appendAction)
alert.addAction(cancelAction)
let gestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(append(sender:)))
gestureRecognizer.minimumPressDuration = 0.0
alert.view.addGestureRecognizer(gestureRecognizer)
return alert
}()
lazy var appendAction: UIAlertAction = {
return UIAlertAction(title: "Paste Message", style: .default, handler: nil)
}()
Make sure your gesture recognizer above is a UILongPressGestureRecognizer set with a minimum press duration of 0. That way you can access the state of the gesture (for when the user touches down) before the action is triggered fully. There you can disable the UIAlertAction, implement your custom code, and reenable the action after the gesture has completed (user has touched up). See below:
#objc func append(sender: UILongPressGestureRecognizer) {
switch sender.state {
case .began:
appendAction.isEnabled = false
case .ended:
// Do whatever you want with the alert text fields
print(alert.textFields?[0].text)
appendAction.isEnabled = true
default:
return
}
}
Also, make sure that the view controller owning the presentation of this alert conforms to UIGestureRecognizerDelegate in order to recognize simultaneous gestures.
extension YourViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
Then, just present the UIAlertController wherever.
func showAlert() {
self.present(alert, animated: true, completion: nil)
}
This is obviously a hack, but there's no other way that I know to achieve this without a hack since it's not meant to be achieved. For example, the gesture recognizer is tied to the UIAlertController so the user can trigger that method if they tap anywhere on the alert (besides the cancel button).
ORIGINAL ANSWER:
This is as close as I could come to a hack-a-round. If there was some way to customize the dismissal transition time to nothing then you could set animated: to false and it would look like the same alert, but I don't think it's possible
class ViewController: UIViewController {
#IBAction func alert(sender: AnyObject) {
let alert = UIAlertController(title: "title", message: "message", preferredStyle: .Alert)
alert.addTextFieldWithConfigurationHandler(nil)
let appendAction = UIAlertAction(title: "Append text", style: .Default) { _ in
var textField = alert.textFields![0] as UITextField
// Append text here
self.presentViewController(alert, animated: true, completion: nil)
}
let cancelAction = UIAlertAction(title: "Cancel", style: .Cancel, handler: nil)
alert.addAction(appendAction)
alert.addAction(cancelAction)
self.presentViewController(alert, animated: true, completion: nil)
}
}
I'm only familiar with swift
Pretty much the same question is answered here
Text field on alert supports paste option, so there is no real reason to have separate button on alert to indicate "paste" option.
Otherwise you should mimic UIAlertController and reimplement it with desiread behavior.
I have a TableViewCell populated with CustomCells. In the customCells I have a button which I want to trigger an UIAlert.
Here is my code for the button in the CustomCell Class:
#IBAction func anzahlButton(sender: UIButton) {
let alert = UIAlertController(title: "Bitte gib Deine gewünschte Anzahl an:", message: nil, preferredStyle: .Alert)
alert.addTextFieldWithConfigurationHandler {
(tf:UITextField!) in
tf.keyboardType = .NumberPad
tf.addTarget(self, action: "textChanged:", forControlEvents: .EditingChanged)
}
func handler(act:UIAlertAction!) {
let tf = alert.textFields![0] as UITextField
let addItem = "\(tf.text)"
var fixedToDoItems = ""
anzahlText.setTitle("\(addItem)", forState: .Normal)
//println("User entered \(addItem), tapped \(act.title)")
}
alert.addAction(UIAlertAction(title: "Cancel", style: .Cancel, handler: nil))
alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: handler))
(alert.actions[1] as UIAlertAction).enabled = false
TableViewController().presentViewController(alert, animated: true, completion: nil)
}
When I hit the button I get this alert: Warning: Attempt to present <UIAlertController: 0x7fc7f0cbc5c0> on <MyApp.TableViewController: 0x7fc7f0c965f0> whose view is not in the window hierarchy!
I deed some resaerch on the debug-alert, but couldn't find an answer that is in swift and works for me... ;-)
Any ideas?
THX
//Seb
In your line TableViewController().presentViewController, TableViewController() actually creates a new instance of the view controller TableViewController. This instance is not the one currently displayed on the screen. Which is why you get the error that the view is not part of the window hierarchy.
In order to fix that move the func anzahlButton(sender: UIButton) to the TableViewController file and then connect it to the button through cellForRowAtIndexPath function. remove the #IBAction part since we are no longer connecting the button through the interface builder.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
// Your Code
cell.button.addTarget(self, action: "anzahlButton:", forControlEvents: UIControlEvents.TouchUpInside)
}
Then change the line
TableViewController().presentViewController(alert, animated: true, completion: nil)
to
self.presentViewController(alert, animated: true, completion: nil)