Swift: UIAlertController in SwiftUI sheet bug - swift

I would like to include UIAlertController in my SwiftUI project because I have more customization options with UIAlertController than with swiftui alert. I've already partially done that with this code:
struct CustomAlertManager {
static let shared = CustomAlertManager()
private init() {}
func showAlert() {
let alert = UIAlertController(title: "Achtung", message: "Sie werden weiter geleitet", preferredStyle: .alert)
let okAction = UIAlertAction(title: "Ok", style: .default) { action in
}
alert.addAction(okAction)
if let controller = UIApplication.shared.rootViewController {
controller.present(alert, animated: true)
}
}
}
My UIApplication extension:
extension UIApplication {
var rootViewController: UIViewController? {
let scene = self.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene
return scene?.keyWindow?.rootViewController
}
}
But now to my problem:
This works pretty much fine but when I want to present an UIAlertController inside of a sheet the alert is not displayed (maybe its is unter the sheet). Note: No, the controller (UIApplication.shared.rootViewController) is not empty, so the .present(_:) method will be called 👍.
Best regards!

Related

SwiftUI: UIAlertController ActionSheet is full screen

I am trying to implement a check marked action sheet in a SwiftUI View. I am using a
UIViewControllerRepresentable to create a UIAlertController
struct WhatsAppAlertController: UIViewControllerRepresentable {
let viewModel: PropViewModel
func makeUIViewController(context: Context) -> UIAlertController {
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let contactsNumbers = viewModel.contactsNumbers()
for number in contactsNumbers {
let action = UIAlertAction(
title: "\(number.value.stringValue)",
style: .default,
handler: { _ in
self.viewModel.openWhatsAppURL(withNumber: number.value.stringValue)
})
alert.addAction(action)
}
let cancel = UIAlertAction(title: L10n.cancel, style: .cancel, handler: nil)
alert.addAction(cancel)
return alert
}
func updateUIViewController(_ uiViewController: UIAlertController, context: Context) {
}
}
It is displayed using
.sheet(isPresented: $showWhatsAppActionSheet) {
WhatsAppAlertController(viewModel: self.viewModel)
}
I have a feeling it is because the UIAlertController is being presented using .sheet
My plan was to use action.setValue(true, forKey: "checked") to checkmark and remember the selected option.
Is there a way to fix this? Or perhaps implement the checkmark using only SwiftUI?
My mistake was not creating a holder controller, a UIViewController to house the UIAlertController. The presentation should also be done with .background() rather than .sheet()
Here is the updated code:
struct WhatsappAlertController: UIViewControllerRepresentable {
#Binding var show: Bool
let viewModel: PropViewModel
func makeUIViewController(context: UIViewControllerRepresentableContext<WhatsappAlertController>) -> UIViewController {
return UIViewController() // holder controller - required to present alert
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<WhatsappAlertController>) {
if self.show {
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let contactsNumbers = viewModel.contactsNumbers()
for number in contactsNumbers {
let action = UIAlertAction(
title: "\(number.value.stringValue)",
style: .default,
handler: { _ in
self.viewModel.openWhatsAppURL(withNumber: number.value.stringValue)
})
// action.setValue(true, forKey: "checked")
alert.addAction(action)
}
let cancel = UIAlertAction(title: L10n.cancel, style: .cancel, handler: nil)
alert.addAction(cancel)
DispatchQueue.main.async { // must be async !!
uiViewController.present(alert, animated: true, completion: {
self.show = false // hide holder after alert dismiss
})
}
}
}
}
And to display:
.background(WhatsappAlertController(show: self.$showWhatsAppActionSheet, viewModel: viewModel))

How do I present a UIAlertController in SwiftUI?

In UIKit, it is common to present UIAlertController for modal pop up alert messages in response to some action.
Is there a modal alert controller type in SwiftUI?
Is there a way to present a UIAlertController from SwiftUI classes? It seems like this may be possible using UIViewControllerRepresentable but not sure if that is required?
This just works:
class func alertMessage(title: String, message: String) {
let alertVC = UIAlertController(title: title, message: message, preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default) { (action: UIAlertAction) in
}
alertVC.addAction(okAction)
let viewController = UIApplication.shared.windows.first!.rootViewController!
viewController.present(alertVC, animated: true, completion: nil)
}
Put it in a Helper-Class.
Usage:
Helper.alertMessage(title: "Test-Title", message: "It works - even in SwiftUI")
Use Alert instead.
import SwiftUI
struct SwiftUIView: View {
#State private var showAlert = false;
var body: some View {
Button(action: { self.showAlert = true }) {
Text("Show alert")
}.alert(
isPresented: $showAlert,
content: { Alert(title: Text("Hello world")) }
)
}
}
Bind to isPresented in order to control the presentation.
I'm using extension of UIViewController to get current vc and UIAlertController to present 'Alert'. Maybe you can try this as followings:
extension for UIViewController
extension UIViewController {
class func getCurrentVC() -> UIViewController? {
var result: UIViewController?
var window = UIApplication.shared.windows.first { $0.isKeyWindow }
if window?.windowLevel != UIWindow.Level.normal {
let windows = UIApplication.shared.windows
for tmpWin in windows {
if tmpWin.windowLevel == UIWindow.Level.normal {
window = tmpWin
break
}
}
}
let fromView = window?.subviews[0]
if let nextRespnder = fromView?.next {
if nextRespnder.isKind(of: UIViewController.self) {
result = nextRespnder as? UIViewController
result?.navigationController?.pushViewController(result!, animated: false)
} else {
result = window?.rootViewController
}
}
return result
}
}
extension for UIAlertController
extension UIAlertController {
//Setting our Alert ViewController, presenting it.
func presentAlert() {
ViewController.getCurrentVC()?.present(self, animated: true, completion: nil)
}
func dismissAlert() {
ViewController.getCurrentVC()?.dismiss(animated: true, completion: nil)
}
}
And you can create your showAlert function now
func showMyAlert() {
let myAlert = UIAlertController(title: "Confirm order", message: "Are you sure to order two box of chocolate?", preferredStyle: .alert)
let okAction = UIAlertAction(title: "Ok!", style: .default) { (_) in
print("You just confirm your order")
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { (_) in
print(You cancel it already!)
}
myAlert.addAction(okAction)
myAlert.addAction(cancelAction)
myAlert.presentAlert()
}
Wish my answer can help you. :-)
You can present UIKit Alert in SwiftUI using notificationCenter
At SceneDelegate.swift on "func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)" insert this code {
NotificationCenter.default.addObserver(self, selector: #selector(self.showAlert), name: Notification.Name("showAlert"), object: nil)
and add this function
#objc private func showAlert(notification: NSNotification){
let msg: String = notification.object as! String
let alert = UIAlertController(title: "Title", message: msg, preferredStyle: .alert)
let cancelAction = UIAlertAction(title: "οκ", style: .cancel) { (action) in
}
alert.addAction(cancelAction)
DispatchQueue.main.async {
self.window?.rootViewController?.present(alert, animated: true, completion: nil)
}
}
Now you can make the app to appear a message with UIKit AlertController writing this code on an action in a swifui class
var body: some View {
Button(action:{
NotificationCenter.default.post(name: Notification.Name("showAlert"), object: "Ελέγξτε το δίκτυο σας και προσπαθήστε αργότερα.")
}
}

UIAlertController is disappearing automatically after short time

If a textfield is empty, I would like to make an UIAlertController to remind, that something has to be filled out in the textfield. Now I have the following code:
#IBAction func saveDetails(segue: UIStoryboardSegue) {
let dController = segue.source as? EntryTableViewController
if let text = dController?.bezeichnungTextField.text, text.isEmpty {
let alert = UIAlertController(title: "No description", message: "Please fill out description", preferredStyle: .alert)
let cancelAction = UIAlertAction(title: "OK", style: .default)
alert.addAction(cancelAction)
self.present(alert, animated: true, completion: nil)
}else {
guard let menge = dController?.mengeTextField.text, let bezeichnung = dController?.bezeichnungTextField.text, let kategorie = dController?.categoryButtonOutlet.titleLabel?.text else {return}
self.saveItem(menge: menge, bezeichnung: bezeichnung, kategorie: kategorie)
self.tableView.reloadData()
}
}
Now actually it works when I press the button to return to the first controller. But the UIAlertController only appears for a very short moment and then disappears automatically. Is there a mistake in my code or isn't it possible to call the UIAlertController on a unwind segue?
Thank you for your help
You just show the alert controller and then immediately unwind. The solution is to check for the empty textfield before unwinding.
Make IBAction for your button then check the textfield if it is empty and then perforn your segue programmatically.

How to show an alert from another class in Swift?

I have a main class, AddFriendsController, that runs the following line of code:
ErrorReporting.showMessage("Error", msg: "Could not add student to storage.")
I then have this ErrorReporting.swift file:
import Foundation
class ErrorReporting {
func showMessage(title: String, msg: String) {
let alert = UIAlertController(title: title, message: msg, preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}
}
Obviously, self wouldn't work here, and is giving me an error. How can I refer to the currently open view controller (i.e. AddFriendsController in this circumstance), as I am wishing to use this same method in many different swift files?
Thanks.
You can create extension method for UIApplication (for example) which will return your topViewController:
extension UIApplication {
static func topViewController(base: UIViewController? = UIApplication.sharedApplication().delegate?.window??.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(nav.visibleViewController)
}
if let tab = base as? UITabBarController, selected = tab.selectedViewController {
return topViewController(selected)
}
if let presented = base?.presentedViewController {
return topViewController(presented)
}
return base
}
}
And then your class will look like this:
class ErrorReporting {
static func showMessage(title: String, msg: String) {
let alert = UIAlertController(title: title, message: msg, preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: nil))
UIApplication.topViewController()?.presentViewController(alert, animated: true, completion: nil)
}
}
Method need to be static to be able to call it as ErrorReporting.showMessage.
Actually, in my opinion the view controller presenting operation should be done on the UIViewController instance, not in a model class.
A simple workaround for it is to pass the UIViewController instance as a parameter
class ErrorReporting {
func showMessage(title: String, msg: String, `on` controller: UIViewController) {
let alert = UIAlertController(title: title, message: msg, preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: nil))
controller.presentViewController(alert, animated: true, completion: nil)
}
}
And call it like below
ErrorReporting.showMessage("Error", msg: "Could not add student to storage.", on: self)
Swift 3 version of Maksym Musiienko's answer would be the following:
extension UIApplication {
static func topViewController(base: UIViewController? = UIApplication.shared.delegate?.window??.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController, let selected = tab.selectedViewController {
return topViewController(base: selected)
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}

Swift expecting declaration but already declared

I am trying to create an alert view that performs a certain action when the button is clicked. I have tried creating a new class for the alert view, but when I try to add an action to the alert view controller, Xcode tells me that it is expecting a declaration, although the variable is declared just two steps above the line where the error occurs. Here's the code
class alerts: UIAlertController {
var alertThenGenerateNewThingController: UIAlertController = UIAlertController()
var generateNewThingOkButton = UIAlertAction (title: "OK", style: UIAlertActionStyle.Default) {
UIAlertAction in
println ("generate new thing action")
}
alertThenGenerateNewThingController.addAction (generateNewThingOkButton) // Here is where Xcode says it expected a declaration
func alertThenGenerateNewThing (alertTitle: String, alertMessage: String) {
alertThenGenerateNewThingController = UIAlertController (title: alertTitle, message: alertMessage, preferredStyle: .Alert)
self.presentViewController (alertThenGenerateNewThingController, animated: true, completion: nil)
}
}
You can't subclass UIAlertController, for one thing. Second of all, you can only interact with object properties inside of methods, functions, or the global scope. Run this code inside of the view controller where you plan on presenting your alert:
class viewController: UIViewController {
var alertThenGenerateNewThingController: UIAlertController = UIAlertController()
var generateNewThingOkButton = UIAlertAction (title: "OK", style: UIAlertActionStyle.Default) {
UIAlertAction in
println ("generate new thing action")
}
override func viewDidLoad() {
alertThenGenerateNewThingController.addAction(generateNewThingOkButton)
}
func alertThenGenerateNewThing (alertTitle: String, alertMessage: String) {
alertThenGenerateNewThingController = UIAlertController (title: alertTitle, message: alertMessage, preferredStyle: .Alert)
self.presentViewController (alertThenGenerateNewThingController, animated: true, completion: nil)
}
}