Use of closure passed into a function causes a malloc crash - ios

I have a custom UIView controller that I init like so:
inputPhoneNumberView.frame = CGRect(x: 0, y: 0, width: 286, height: 73)
let alert = SAAlertView(title: "Enter Your Phone Number", message: "Enter or update your phone number and we will send you a verification code via SMS.", customView: inputPhoneNumberView)
alert.addAction("Confirm", style: .default, dismissAfterAction: false, hasLoadingIndicator: true) { () -> Void in
print("Hello!") //Testing code
}
alert.addAction(NSLocalizedString("cancel", comment: ""), style: .cancel, actionBlock: nil)
present(alert, animated: true, completion: nil)
Actions can be added to the view controller that are associated to UIButtons inside it. e.g. alert.addAction(NSLocalizedString("cancel", comment: ""), style: .cancel, actionBlock: nil
An action is a struct defined as so:
struct Action {
var title: String
var style: ActionStyle
var actionBlock: (() -> Void)?
var dismissAfterAction: Bool
var hasLoadingIndicator: Bool
}
The code block in an action is handled in this method:
fileprivate dynamic func doAction(_ sender: CustomButton) {
// Make sure the action should be allowed for default only.
let action = actions[sender.tag]
guard sender.tag >= 0 && sender.tag < actions.count else {
print("No action at that index.", logType: .Error)
return
}
if let block = action.actionBlock {
if action.dismissAfterAction {
dismiss(animated: true, completion: {
block()
})
} else {
block() // The point the crash occurs!
}
} else {
dismiss(animated: true, completion: nil)
}
}
There is an option when creating an action to dismissAfterAction i.e. auto dismiss the view controller then perform the code block. However if dismissAfterAction is false the app will crash with a malloc error. It doesn't always crash but if I repeatedly tap the button associated with that action it will eventually. Not sure what's going on here. Has anyone come across anything like this before? Seems to be a problem with the code block.

I've had likely issue, some time ago. As a fix you can change your code in this way:
//we already checked action block, so
action.actionBlock!()
//block() The point the crash occurs!

Related

Swift UIKit / presenting UIAlert blocks pushing VC

I got simple UIViewController + UIAlert extension:
extension UIViewController {
func alert(title: String = "", message: String){
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: Localized.ok(), style: .default, handler: nil))
present(alert, animated: true, completion: nil)
}
}
Within the ViewController I got a method:
func findUser() {
userService.findUser { (userinfo, error) in
if error != nil {
if let errText = error?.localizedDescription {
self.alert(message: errText)
}
self.doAuth()
return
}
}
}
this doAuth() method should redirect to loginViewController using:
navigationController?.pushViewController(loginViewController, animated: false)
The problem is, that in this scenario, this push doesn't work (nothing appears) (I click OK button on the alert, alert dissapears but loginViewController is not pushed)
I refactored extension a little bit:
extension UIViewController {
func alert(title: String = "", message: String, action completion: (() -> Void)? = nil){
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: Localized.ok(), style: .default, handler: { _ in
completion?()
}))
present(alert, animated: true, completion: nil)
}
}
so findUser() method is calling doAuth() in differently:
func findUser() {
userService.findUser { (userinfo, error) in
if error != nil {
if let errText = error?.localizedDescription {
self.alert(message: errText){ [weak self] in
self?.doAuth()
}
}
return
}
}
}
and it works!
Problem is I have no idea why. And what could have happened in the first scenario?
I feel it should be some simple explanation, but I can't figure it out.
Edit
The explanation is simple and was printed in the console:
pushViewController:animated: called on <UINavigationController 0x7f86050b4400>
while an existing transition or presentation is occurring; the navigation stack
will not be updated.
So doAuth() (with pushing VC method) was called while alert was visible/presented, so alert took the focus and VC couldn't be pushed.
cc: #Paulw11 # cookednick
Problem is navigating and presenting in same if statement
If error != nil , means it only show the alert not try to doAuth().
But you are calling doAuth() in same if block then it is trying to present alert as well as to navigate
func findUser() {
userService.findUser { (userinfo, error) in
if error != nil {
if let errText = error?.localizedDescription {
self.alert(message: errText)
}
return
}
//Out side if block
self.doAuth()
}
}

how can i do it: library's callback wait until user choose from popover and then get return

I have a problem with understanding how work with view/delegate and completion.
I use library which have callback - something like:
func youShouldChoose()->String.
I desided to give a choice to user and open popover. But I don't understand how to return the selected value.
I read about completion. So i've tried this:
func youShouldChoose() -> String {
askUser()
return self.valueForResult //This line is executed earlier than askUser is finished
}
func askUser(){
showAlert(completion: {(result)->Void in
self.valueForResult = result
})
}
func showAlert(completion:#escaping (_ result:String)->Void)
{
let alert = UIAlertController(...)
alert.addAction(UIAlertAction(title: "Click", style: UIAlertAction.Style.default, handler: { action in
completion(textField.text)
}))
alert.addTextField(configurationHandler: {(textField: UITextField!) in
textField.placeholder = "Enter text:"
})
self.present(alert, animated: true, completion: nil )
}
How can I wait until askUser() will end completely? Is there a way to return value from completion to my library?
I found two ways to solve out this problem:
1. Use loop. Showing view until flag is false
askUser() //we should set flag to true here
while( flag == false ) {
CFRunLoopRunInMode(CFRunLoopMode.defaultMode, 1, true);
}
return self.valueForResult
Use semaphore
let semaphore = DispatchSemaphore(value: 0)
askUser()
semaphore.lock()
return self.valueForResult
Here is an example solution (Swift 4.2 / 5.0):
func youShouldChoose(_ completion: #escaping ((String) -> Void)) {
askUser(completion) // handing over the completion block to `askUser.
// Alternative completion block execution:
// askUser { (enteredText) in
// // This block is called when the "Click" action button on the alert was tapped.
// completion(enteredText)
// }
}
func askUser(_ completion: #escaping ((String) -> Void)) {
showAlert(completion) // handing over the completion block to `showAlert`.
}
func showAlert(_ completion: #escaping (String) -> Void) {
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction.init(title: "Click", style: .default, handler: { (_) in
if let textField = alert.textFields?.first, let text = textField.text {
completion(text) // -> handing over the text of the textField!
} else {
// No text field or text available. Something went wrong!
}
}))
alert.addTextField { (textField) in
textField.placeholder = "Enter text:"
}
self.present(alert, animated: true, completion: nil)
}
// How to use `youShouldChoose `:
func foo() {
youShouldChoose { (enteredText) in
// This block is called when `youShouldChoose` is finished.
print(enteredText) // -> prints the user's entered text.
print("Hello")
}
}

How to detect Add button presses in PKAddPassesViewController

I'm trying to detect when the users tap on the "Add" button in the PKAddPassesViewController.
I added addPassesViewControllerDidFinish() so that when passVC is dismissed, function addPassesViewControllerDidFinish() will be called.
override func viewDidLoad() {
self.pass = try PKPass(data: downloadedData! as Data)
let passVC = PKAddPassesViewController(pass: self.pass)
self.present(passVC!, animated: true)
// when passVC is dimissed by the user, addPassesViewControllerDidFinish is expected to be called, but it never gets called.
}
func addPassesViewControllerDidFinish(_ controller: PKAddPassesViewController) {
print("enter DidFinish")
let passLib = PKPassLibrary()
// Get your pass
guard let pass = self.pass else { return }
if passLib.containsPass(pass) {
print("if start")
// Show alert message for example
let alertController = UIAlertController(title: "", message: "Successfully added to Wallet", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
controller.dismiss(animated: true, completion: nil)
}))
controller.show(alertController, sender: nil)
print("if end")
} else {
// Cancel button pressed
print("else start");
controller.dismiss(animated: true, completion: nil)
print("else end");
}
}
However, when passVC is dimissed by the user, func addPassesViewControllerDidFinish() never gets called at all.
There are three things you should fix:
1. Extend PKAddPassesViewControllerDelegate in your ViewController class.
2. Double check if you added delegate to your PKAddPassesViewController: VC?.delegate = self, which will link your delegate to PKAddPassesViewControllerDelegate.
3. Inside addPassesViewControllerDidFinish, dismiss controller first. Then do whatever you want inside passLib.containPass. The alertController is no longer belonged to the controller, maybe to its parent view.

Prevent presenting the UIAlertViewController after navigating to the other view

I have one scenario when the user did not use the application for more than 5 min app will show a popup with session expiration message.
The code for session expiration is added in the appDelegate and from there the popup will be presented on the current view controller.
code is
#objc func applicationDidTimeout(notification: NSNotification) {
if (window?.rootViewController?.isKind(of: UITabBarController.self))! {
for view in window?.rootViewController?.view.subviews ?? [(window?.rootViewController?.view)!] {
if view.isKind(of: MBProgressHUD.self) {
return
}
}
if window?.rootViewController?.presentedViewController != nil {
window?.rootViewController?.dismiss(animated: true, completion: {
self.showMessage(message: Message.sessionTimeout)
})
} else {
self.showMessage(message: Message.sessionTimeout)
}
}
}
fileprivate func showMessage(message: String) {
let alert = UIAlertController(title: appName, message: message, preferredStyle: .alert)
let actionOkay = UIAlertAction(title: "OK", style: .default) { (action) in
DispatchQueue.main.async {
UIView.transition(with: self.window!, duration: 0.3, options: UIView.AnimationOptions.transitionCrossDissolve, animations: {
CommonFunctions.setLoginAsRootVC()
}, completion: nil)
}
}
alert.addAction(actionOkay)
self.window?.rootViewController?.present(alert, animated: true, completion: nil)
}
Now if the user is doing some data entry and at that time, if the user leaves application ideal for 5 min or more the keyboard will dismiss and the session expiration message shown there.
But as the text field's delegate method textFieldShouldEndEditing has some validation and if that validation fails it shows a popup with the message and ok button.
So when the user taps on the ok button in the session expiration message popup, it will redirect the user to the login screen but due to the text field's delegate method validation, it shows one pop up in the login screen.
Code for the validation fail message popup is
fileprivate func showErrorMessage(message: String) {
let alert = UIAlertController(title: appName, message: message, preferredStyle: .alert)
let actionOkay = UIAlertAction(title: "OK", style: .default) { (action) in
self.txtField.becomeFirstResponder()
}
alert.addAction(actionOkay)
self.present(alert, animated: true, completion: nil)
}
How to prevent the popup from being present in the login screen?
I try to get the proper way to prevent the popup from appearing on the login screen.
But Finally, I found one heck to solve this issue.
I have declared one boolean in AppDelegate and set it's value to false when I want to prevent the popup from appearing and then revert it back to true when I want to show the popup.
I know this is not the elegant or efficient solution for the issue, but it works for now.
If anyone knows the better answer can post here, I'm still open to any better solution.
#objc func applicationDidTimeout(notification: NSNotification)
{
let visibleView : UIViewController = self.getVisibleViewControllerFrom(self.window?.rootViewController)!
self.showMessage(message: Message.sessionTimeout,Controller: visibleView)
}
fileprivate func showMessage(message: String , Controller : UIViewController) {
let alert = UIAlertController(title: appName, message: message, preferredStyle: .alert)
let actionOkay = UIAlertAction(title: "OK", style: .default) { (action) in
//Now apply your code here to set login view controller as rootview
// This controller is for demo
window!.rootViewController = UIStoryboard(name: "Main", bundle:
nil).instantiateViewController(withIdentifier: "loginview")
window!.makeKeyAndVisible()
}
alert.addAction(actionOkay)
Controller.present(alert, animated: true, completion: nil)
}
//MARK:- Supporting method to get visible viewcontroller from window
func getVisibleViewControllerFrom(_ vc: UIViewController?) -> UIViewController? {
if let nc = vc as? UINavigationController {
return self.getVisibleViewControllerFrom(nc.visibleViewController)
} else if let tc = vc as? UITabBarController {
return self.getVisibleViewControllerFrom(tc.selectedViewController)
} else {
if let pvc = vc?.presentedViewController {
return self.getVisibleViewControllerFrom(pvc)
} else {
return vc
}
}
}
Try this code, I've use this code many times may be it's work for you.

CocoaAction with RxSwift and UIAlertController

I'm trying to implement the current behavior with a textfield and a button:
1 - the textfield should be validated not realtime but only after tapping the button it has to show an error label for the validation error
2 - if the textfield is validate I have to show an uialertcontroller to cancel or continue the operation
I tried especially the second part with the following code but It works only the first time, if I tap cancel for example and I tap an other time the button it looks like disabled....no more taps are allowed.
let action = CocoaAction {
return Observable.create {
[weak self] observer -> Disposable in
let alertController = self.getAlertController()
let ok = UIAlertAction.Action(NSLocalizedString("OK_BUTTON", comment: "OK_BUTTON"), style: .Default)
ok.rx_action = CocoaAction { _ in
return self!.viewModel!.modify(self?.addressTextFiled.rx_text)
.doOnNext({ data in
if let data = data
{
self!.showMessage(data.message)
}
})
.map { _ in Void() }
}
let cancelAction = UIAlertAction(title: NSLocalizedString("CANCEL_BUTTON", comment: "CANCEL_BUTTON"), style: .Cancel) { (_) in }
alertController.addAction(ok)
alertController.addAction(cancelAction)
self!.presentViewController(alertController, animated: true, completion: nil)
return NopDisposable.instance
}
}
confirmButton.rx_action = action
For the first point do you have some advise?
Thanks to help me out!!
In the viewModel the observable returned is:
func modify() -> Observable<StatusResponse?>
{
return input!.continueClick
.withLatestFrom(requestData!)
.flatMap{ [unowned self] (code, mail) -> Observable<StatusResponse?> in
return self.provider.request(APIProvider.ModifyRequest(code, "A", mail))
.mapObjectOptional(ModifyStatusResponse.self)
.trackActivity(self.activityIndicator)
}
.retry()
.shareReplay(1)
}
What does avoid the completion of the observable?

Resources