So, I added this extension function to the PrimitiveSequenceType to show a loader on screen when making a network call
extension PrimitiveSequenceType where Trait == SingleTrait {
func subscribeWithLoader(showLoaderOn viewController: MyUIViewController, onSuccess: ((Element) -> Void)? = nil, onFailure: ((Swift.Error) -> Void)? = nil)-> Disposable {
let loader = viewController.showLoading()
return subscribe { (element) in
DispatchQueue.main.async {
loader.dismiss(animated: true, completion: {
onSuccess?(element)
})
}
} onFailure: { (error) in
DispatchQueue.main.async {
loader.dismiss(animated: true, completion: {
onFailure?(error)
})
}
}
}
}
Here is my showLoading function
func showLoading()-> UIAlertController {
let alert = UIAlertController(title: nil, message: "Please wait...", preferredStyle: .alert)
let loadingIndicator = UIActivityIndicatorView(frame: CGRect(x: 10, y: 5, width: 50, height: 50))
loadingIndicator.hidesWhenStopped = true
loadingIndicator.style = UIActivityIndicatorView.Style.medium
loadingIndicator.startAnimating();
alert.view.addSubview(loadingIndicator)
present(alert, animated: true, completion: nil)
return alert
}
But the loader never stops. Can anybody let me know what I'm doing wrong. Any help would be appreciated.
It's not very Rx like, but it works... except for one edge case. You dismiss the alert on success and on failure, but what if the Single is disposed without emitting either? Then the alert won't dismiss.
Try something like this instead:
extension PrimitiveSequenceType where Trait == SingleTrait {
func withLoader(showLoaderOn viewController: UIViewController) -> Single<Element> {
func loadingController() -> UIAlertController {
let alert = UIAlertController(title: nil, message: "Please wait...", preferredStyle: .alert)
let loadingIndicator = UIActivityIndicatorView(frame: CGRect(x: 10, y: 5, width: 50, height: 50))
loadingIndicator.hidesWhenStopped = true
loadingIndicator.style = UIActivityIndicatorView.Style.medium
loadingIndicator.startAnimating();
alert.view.addSubview(loadingIndicator)
return alert
}
return Single.create { fullfil in
let loader = loadingController()
viewController.present(loader, animated: true)
let disposable = self.subscribe(fullfil)
return Disposables.create {
disposable.dispose()
loader.dismiss(animated: true)
}
}
}
}
If you like wrapping view controllers up like this. Check out this library... Cause-Logic-Effect
So, I end up adding a delay like this
extension PrimitiveSequenceType where Trait == SingleTrait {
func subscribeWithLoader(showLoaderOn viewController: MyUIViewController, onSuccess: ((Element) -> Void)? = nil, onFailure: ((Swift.Error) -> Void)? = nil)-> Disposable {
let loader = viewController.showLoading()
return subscribe { (element) in
onSuccess?(element)
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5, execute: {
DispatchQueue.main.async {
loader.dismiss(animated: true, completion: nil)
}
})
} onFailure: { (error) in
onFailure?(error)
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5, execute: {
DispatchQueue.main.async {
loader.dismiss(animated: true, completion: nil)
}
})
} onDisposed: {
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5, execute: {
DispatchQueue.main.async {
loader.dismiss(animated: true, completion: nil)
}
})
}
}
}
I think the problem was onSuccess (that means calling loader.dismiss) was getting called even before UIAlertController could show itself. So, by adding a delay of 500ms solves the issue the UIAlertController is going to have enough time to show itself, and then we are dismissing it.
Open to new ideas and improvements.
Related
I want to ask I have a sign in view controller who don't want to dismiss after correctly add email and password. but when I try the simulator for the first time the sign in is working and directing to me to my home controller, but after I sign out. and try to sign in again, then the sign in not dismissing my sign in view controller. how is that possible? at first is working later on is not working, here I show you my code.
// this is my sign out button
#objc private func handleSignOut() {
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
alert.addAction(UIAlertAction(title: "Log Out".localized(), style: .destructive, handler: { (_) in
self.progressHUD.show(in: self.view)
ProfileServices.shared.signOutUser { success in
if success {
self.progressHUD.dismiss(animated: true)
let signInVC = SigninViewController()
self.present(signInVC, animated: true, completion: nil)
} else {
self.progressHUD.textLabel.text = "Error"
self.progressHUD.dismiss(afterDelay: 0.4)
}
}
}))
alert.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil))
present(alert, animated: true, completion: nil)
}
// this is my sign out function in ProfileServices.shared
func signOutUser(completion: #escaping (Bool) -> Void) {
AF.request(API_URL.AUTHENTICATION.LOGOUT, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: HEADERS, interceptor: nil).responseData { (dataResponse) in
if dataResponse.error == nil {
let domain = Bundle.main.bundleIdentifier!
UserDefaults.standard.removePersistentDomain(forName: domain)
UserDefaults.standard.synchronize()
UserDefaults.removeToken()
completion(true)
} else {
completion(false)
}
}
}
// this is my sign in route in my sign in view controller
func routeToMainView(_ data: SigninModel.Response) {
let school = UserDefaults.getSelectedSchool()
guard let schools = data.schools?.schools else { return }
if let selectedSchool = school, let selected = schools.first(where: { $0.id == selectedSchool.id}) {
UserDefaults.saveSelectedSchool(data: selected)
let vc = MainViewController()
self.viewController?.navigationController?.setViewControllers([vc], animated: true)
} else {
if schools.count > 1 {
let vc = SwitchSchoolViewController()
self.viewController?.navigationController?.setViewControllers([vc], animated: true)
} else {
guard let selected = schools.first else { return }
UserDefaults.saveSelectedSchool(data: selected)
DispatchQueue.main.async {
let vc = MainViewController()
self.viewController?.navigationController?.setViewControllers([vc], animated: true)
}
}
}
}
// this is in my appDelegate
var root: UIViewController?
root = SigninViewController()
if UserDefaults.getToken() != nil {
root = MainViewController()
}
In logout you need to dissmiss the presented viewController.
inplace :
let signInVC = SigninViewController()
self.present(signInVC, animated: true, completion: nil)
you need to use:
self.dismiss(animated: true, completion: nil)
or pop if you will use Push.
I'm trying to alert the user with local notification when randomly generated integer value is more than the label's text value in Swift 4. I have looked for answers in Google even though I implemented them to my problem as required I couldn't solve it. Here is my code:
import UIKit
import Dispatch
import QuartzCore
import UserNotifications
class ViewController: UIViewController {
var alertButton: UIButton {
let button = UIButton(frame: CGRect(x: 100, y: 150, width: UIScreen.main.bounds.width / 2, height: 100))
button.setTitle("Show Alert", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(showAlert), for: .touchUpInside)
return button
}
var valueLabel: UILabel {
let label = UILabel(frame: CGRect(x: 250, y: 300, width: 150, height: 150))
label.text = "Value"
label.textColor = UIColor.black
return label
}
var value: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(alertButton)
self.view.addSubview(valueLabel)
UNUserNotificationCenter.current().delegate = self
requestSettings()
//let link = CADisplayLink(target: self, selector: #selector(updateLabels))
//link.add(to: .main, forMode: .default)
Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(self.updateLabels), userInfo: nil, repeats: true)
}
private func requestAuthorization(completionHandler: #escaping (_ success: Bool) -> ()) {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge]) { (success, error) in
if let error = error {
print("Auth request failed. \(error) \(error.localizedDescription)")
}
completionHandler(success)
}
}
private func requestSettings() {
UNUserNotificationCenter.current().getNotificationSettings { (notificationSettings) in
switch notificationSettings.authorizationStatus {
case .notDetermined:
self.requestAuthorization { (success) in
guard success else { return }
self.scheduleNotification()
}
case .denied:
print("App not allowed to display notification")
case .authorized:
self.scheduleNotification()
case .provisional:
self.scheduleNotification()
}
}
}
#objc func updateLabels() {
self.value = Int.random(in: 1...50)
print("\(self.value)")
DispatchQueue.main.async {
self.valueLabel.text = "\(self.value)"
}
}
#objc func showAlert() {
var val: String = ""
let alert = UIAlertController(title: "Threshold", message: nil, preferredStyle: .alert)
alert.addTextField(configurationHandler: { (textField) in
textField.placeholder = "Enter an integer value:"
})
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (action) in
val = (alert.textFields?.first!.text)!
if self.value > Int(val)! {
self.scheduleNotification()
}
alert.dismiss(animated: true, completion: nil)
}))
self.present(alert, animated: true, completion: nil)
}
private func scheduleNotification() {
let content = UNMutableNotificationContent()
content.title = "App"
content.subtitle = "WARNING!"
content.body = "Threshold value is passed"
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(identifier: "notification_id", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { (error) in
if let error = error {
print("\(error) \(error.localizedDescription)")
}
}
}
}
extension ViewController: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void)
{
completionHandler([.alert])
}
}
Any help is appreciated.
Update: I tried implementing local push notification. But my problem is that i can't push it to the user depending on the condition i mentioned.
I'm using a loading modal pretty much according to this topic:
Loading an "overlay" when running long tasks in iOS
I use the same code in several ViewControllers, so I created an extension:
extension UIViewController {
func showLoading() {
let alert = UIAlertController(title: nil, message: "Please wait...", preferredStyle: .alert)
let loadingIndicator = UIActivityIndicatorView(frame: CGRect(x: 10, y: 5, width: 50, height: 50))
loadingIndicator.hidesWhenStopped = true
loadingIndicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.gray
loadingIndicator.startAnimating();
alert.view.addSubview(loadingIndicator)
present(alert, animated: false, completion: nil)
}
func hideLoading() {
if ( presentedViewController != nil && !presentedViewController!.isBeingPresented ) {
dismiss(animated: false, completion: nil)
}
}
}
I typically use the code like this:
self.showLoading()
callNetwork() { response in
DispatchQueue.main.async {
self.hideLoading()
....
}
}
If the network call takes 0.5s or more, everything works fine. The issue is if the network is too fast. Then I'll get an error similar to this one:
Warning: Attempt to dismiss from view controller <UINavigationController: 0x7ff581830a00> while a presentation or dismiss is in progress!
And the modal won't get dismissed.
The best solution I can come up with is something like this (super class instead of extension as extensions can't have variables):
class LoadingViewController: UIViewController {
var shouldDismissImmediately = false
func showLoading() {
shouldDismissImmediately = false
let alert = UIAlertController(title: nil, message: "Please wait...", preferredStyle: .alert)
let loadingIndicator = UIActivityIndicatorView(frame: CGRect(x: 10, y: 5, width: 50, height: 50))
loadingIndicator.hidesWhenStopped = true
loadingIndicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.gray
loadingIndicator.startAnimating();
alert.view.addSubview(loadingIndicator)
present(alert, animated: false) {
if (self.shouldDismissImmediately) {
self.dismiss(animated: false, completion: nil)
}
}
}
func hideLoading() {
if ( presentedViewController != nil && !presentedViewController!.isBeingPresented ) {
dismiss(animated: false, completion: nil)
} else {
shouldDismissImmediately = true
}
}
}
Can anyone think of a better solution? This one just doesn't feel right. Maybe I'm doing something fundamentally wrong. Like - should I even present such a dialog when waiting for a network response? Is there a better way of making the user to wait? I need the user to be aware that something is happening and in the same time, I need him not to be able to press any buttons in the UI.
extension UIViewController {
func showLoading(finished: #escaping () -> Void) {
let alert = UIAlertController(title: nil, message: "Please wait...", preferredStyle: .alert)
let loadingIndicator = UIActivityIndicatorView(frame: CGRect(x: 10, y: 5, width: 50, height: 50))
loadingIndicator.hidesWhenStopped = true
loadingIndicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.gray
loadingIndicator.startAnimating();
alert.view.addSubview(loadingIndicator)
present(alert, animated: false, completion: finished)
}
func hideLoading(finished: #escaping () -> Void) {
if ( presentedViewController != nil && !presentedViewController!.isBeingPresented ) {
dismiss(animated: false, completion: finished)
}
}
}
self.showLoading(finished: {
callNetwork() {
DispatchQueue.main.async {
self.hideLoading(finished: {
// done
})
}
}
})
I have created BaseViewController to use as subclass for all of my view controllers so that I can show alert whenever I need any progess to show and hide which is lazy variable.
Everything is cool until now. But I figured out that all my viewcontroller which are inherting from this are not releasing. What is the problem?
class BaseViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor(red: 0.4 / 255.0, green: 100 / 215.0, blue: 120 / 255.0, alpha: 1.0)
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return preferredStatusBarStyle_Internal()
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return supportedInterfaceOrientations_Internal()
}
lazy var progressHUD: MBProgressHUD = {
if let navController = self.navigationController {
return navController.HUD
}
return self.HUD
}()
func showAlert(_ title: String?, message: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
let action = UIAlertAction(title: "OK", style: .cancel, handler: { _ in
DispatchQueue.main.async {
self.progressHUD.hide(animated: false, afterDelay: 1.0)
}
})
alert.addAction(action)
present(alert, animated: true, completion: nil)
}
func showPermissionDeniedAlert(_ message: String) {
let alertController = UIAlertController(title: message, message: "Go to Settings?".localized, preferredStyle: .alert)
let settingsAction = UIAlertAction(title: "Settings".localized, style: .default) { _ in
guard let settingsUrl = URL(string: UIApplicationOpenSettingsURLString) else {
return
}
if UIApplication.shared.canOpenURL(settingsUrl) {
if #available(iOS 10.0, *) {
UIApplication.shared.open(settingsUrl, completionHandler: { success in
print("Settings opened: \(success)") // Prints true
})
} else {
let success = UIApplication.shared.openURL(settingsUrl)
print("Settings opened: \(success)")
}
}
}
alertController.addAction(settingsAction)
let cancelAction = UIAlertAction(title: "Cancel", style: .default, handler: nil)
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
}
}
extension UIViewController {
func preferredStatusBarStyle_Internal() -> UIStatusBarStyle {
return .lightContent
}
func supportedInterfaceOrientations_Internal() -> UIInterfaceOrientationMask {
return isiPad() ? .allButUpsideDown : .all
}
var HUD: MBProgressHUD {
let progressHUD = MBProgressHUD(viewController: self)
return progressHUD
}
}
If the view controller is not releasing it means you are creating retain cycle.
It could be in two places
pass weakself to async block.
DispatchQueue.main.async { [weak self] in
self?.progressHUD.hide(animated: false, afterDelay: 1.0)
}
passing a weak reference if MBProgressHUD init is creating retain cycle
var HUD: MBProgressHUD {
// you can also use weak self
unowned let unownedSelf = self
let progressHUD = MBProgressHUD(viewController: unownedSelf)
return progressHUD
}
I'm trying to authenticate login by retrieving a boolean from my web server using URLSession, and show an Alert Controller if the login fails.
func requestLogin() {
let url = URL(string: "http://mywebserver/login.php")
var request = URLRequest(url: url!)
request.httpMethod = "POST"
let postString = "username=\(txtUsername.text!)&password=\(txtPassword.text!)"
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
guard data != nil else {
self.promptMessage(message: "No data found")
return
}
do {
if let jsonData = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSDictionary {
let success = jsonData.value(forKey: "success") as! Bool
if (success) {
self.dismiss(animated: false, completion: { action in
//Move to next VC
})
return
} else {
self.dismiss(animated: false, completion: { action in
self.promptMessage(message: "The username or password that you have entered is incorrect. Please try again.")}
)
return
}
} else {
self.dismiss(animated: false, completion: { action in
self.promptMessage(message: "Error: Could not parse JSON!")
})
}
} catch {
self.dismiss(animated: false, completion: { action in
self.promptMessage(message: "Error: Request failed!")
})
}
})
showOverlayOnTask(message: "Logging in...")
task.resume()
}
func promptMessage(message: String) {
let alert = UIAlertController(title: "Login Failed", message: message, preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
self.present(alert, animated: true, completion: nil)
}
func showOverlayOnTask(message: String) {
let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
let loadingIndicator = UIActivityIndicatorView(frame: CGRect(x: 10, y: 5, width: 50, height: 50))
loadingIndicator.hidesWhenStopped = true
loadingIndicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.gray
loadingIndicator.startAnimating();
alert.view.addSubview(loadingIndicator)
self.present(alert, animated: true, completion: nil)
}
The weird problem I'm getting is that my Logging In alert controller sometimes does not dismiss. It gets stuck until I tap on the screen, which then will dismiss and show the next alert controller. It's very annoying and I don't know where I'm doing wrong.
How do I fix this?
Maybe the problem is that you're trying to dismiss the controller without executing on the main thread, normally the UI changes/updates should be executed on the main thread.
Try this and check if works:
DispatchQueue.main.async {
self.dismiss(animated: false, completion: { action in
self.promptMessage(message: "Error: Could not parse JSON!")
})
}