I have a situation where I would like to recreate previous UIKit logic by displaying an Alert during a long running operation that the user cannot interact with.
Previously in UIKit, it would be this simple:
import UIKit
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let alertController = UIAlertController(title: "Loading",
message: nil,
preferredStyle: .alert)
present(alertController, animated: true, completion: nil)
}
}
And it looks like this:
A simple SwiftUI based version can be created as such:
import SwiftUI
struct ContentView: View {
#State private var isLoading = true
var body: some View {
Text("Hello")
.modifier(LoadingAlert(isPresented: $isLoading))
}
}
struct LoadingAlert: ViewModifier {
#Binding var isPresented: Bool
func body(content: Content) -> some View {
content
.alert(isPresented: $isPresented) {
Alert(title: Text(NSLocalizedString("Loading", comment: "")),
dismissButton: .none)
}
}
}
Whether I use nil or .none as the dismissButton argument, or completely omit the line, it will use a default OK button, and produce this:
I did modify the Alert arguments, sending in a button with an empty title, but this is the result, which is not as clean as I would like:
dismissButton: .default(Text("")))
Based on what I have seen, it does not appear that the Alert in SwiftUI supports what I want, based upon inspecting its initializers.
/// Creates an alert with one button.
public init(title: Text, message: Text? = nil, dismissButton: Alert.Button? = nil)
/// Creates an alert with two buttons.
///
/// - Note: the system determines the visual ordering of the buttons.
public init(title: Text, message: Text? = nil, primaryButton: Alert.Button, secondaryButton: Alert.Button)
For this project, the goal is to fully utilize SwiftUI, but it appears this is a scenario where we cannot get the desired result.
My take is either we will have to pull in a UIKit based AlertController, or use a different effect to indicate status. However, I'd love to be wrong here.
I don't think an alert without a dismiss button in currently supported in SwiftUI, but you could create a custom view and present it with the same effect.
This library might help you out: https://github.com/exyte/PopupView
You can try this:
var dialogMessage = UIAlertController(title: "Erro", message: "error description", preferredStyle: .alert)
let window = UIApplication.shared.keyWindow
window?.rootViewController?.present(dialogMessage, animated: true)
}
Related
I am in the process of building a Swift app, and am trying to figure out how to display alerts. I have a separate swift file that is doing some calculations, and under a certain conditions I want it to display an alert to the user basically telling them something is wrong. However, most of the examples I have seen require the alert to be within the ContentView or otherwise somehow connected to a view, and I can't figure out how to display an alert from a separate file outside of any views.
Most of the examples I have seen look something like this:
struct ContentView: View {
#State private var showingAlert = false
var body: some View {
Button("Show Alert") {
showingAlert = true
}
.alert("Important message", isPresented: $showingAlert) {
Button("OK", role: .cancel) { }
}
}}
If I understand your question correctly, you want to show an alert on the UI when some condition happens in your calculations.
Where the calculations take place somewhere else in your code, eg a task monitoring a sensor.
Here I present an approach, using NotificationCenter as shown in the example code. Whenever and wherever you are in your code, send a NotificationCenter.default.post... as in the example code, and the alert will popup.
class SomeClass {
static let showAlertMsg = Notification.Name("ALERT_MSG")
init() {
doCalculations() // simulate showing the alert in 2 secs
}
func doCalculations() {
//.... do calculations
// then send a message to show the alert in the Views "listening" for it
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
NotificationCenter.default.post(name: SomeClass.showAlertMsg, object: nil)
}
}
}
struct ContentView: View {
let calc = SomeClass() // for testing, does not have to be in this View
#State private var showingAlert = false
var body: some View {
Text("calculating...")
.alert("Important message", isPresented: $showingAlert) {
Button("OK", role: .cancel) { }
}
// when receiving the msg from "outside"
.onReceive(NotificationCenter.default.publisher(for: SomeClass.showAlertMsg)) { msg in
self.showingAlert = true // simply change the state of the View
}
}
}
I am creating an app in SwiftUI on iOS 13 in Xcode 11.6
I want to create an extension on SwiftUI's View that shows an alert message when a user long presses on the view.
For example, suppose I have a view like so:
import SwiftUI
struct TestView: View {
var body: some View {
TabView {
Text("1").addLongPressAlert("Test 1")
Text("2").addLongPressAlert("Test 2")
Text("3").addLongPressAlert("Test 3")
}
}
}
The extension on View would look something like this:
extension View {
public func addLongPressAlert(message _ : String) -> some View {
return self.onLongPressGesture {
// I know this is not how you show an alert, but im unsure how to display it
Alert(title: Text("Alert"), message: Text(m), dismissButton: .default(Text("OK!")))
}
}
}
I am struggling to figure out how to set this up correctly.
Does anyone know how to achieve this?
You can create a custom ViewModifier:
struct LongPressAlertModifier: ViewModifier {
#State var showAlert = false
let message: String
func body(content: Content) -> some View {
content
.onLongPressGesture {
self.showAlert = true
}
.alert(isPresented: $showAlert) {
Alert(title: Text("Alert"), message: Text(message), dismissButton: .default(Text("OK!")))
}
}
}
and use it like this:
Text("1").modifier(LongPressAlertModifier(message: "Test1"))
You can even create a custom View extension:
extension View {
func addLongPressAlert(_ message: String) -> some View {
self.modifier(LongPressAlertModifier(message: message))
}
}
and use your modifier in a more convenient way:
Text("1").addLongPressAlert("Test 1")
For some reason, the following code is displaying an Alert with three instances of the same button, none of which trigger the action (just a simple console output for an example) as expected:
Has anyone else experienced this? Any suggestions on a fix?
It's building on Xcode 11.2.1, for an iOS 13.0 target, then running on macOS (10.15.1) via Catalyst.
Update 1: This appears to be an issue specific to Catalyst. When the same code is run on an iPhone simulator, it shows one button and executes the action, as expected.
Update 2: The issue also wasn't fixed by updating to Xcode 11.3.1 and macOS 10.15.3.
public struct ContactUsView: View {
#ObservedObject private var contactUsVM: ContactUsViewModel
private var successAlert: Alert {
Alert(
title: Text("Email Sent"),
message: Text("Thanks for taking the time to reach out to us. We appreciate it!"),
dismissButton: .default(Text("OK")) {
self.dismissSelf()
}
)
}
public var body: some View {
Form {
// ...
}
.alert(isPresented: self.$contactUsVM.contactAttemptSucceeded) {
self.successAlert
}
}
public init() {
self.contactUsVM = ContactUsViewModel()
}
private func dismissSelf() {
print("Dismissing!")
}
}
class ContactUsViewModel: ObservableObject {
#Published var contactAttemptSucceeded: Bool = true
}
It seems that your code works fine on xCode 11.5 MacOs 0.15.4. If you run your example (I've just filled the hole in your code):
import SwiftUI
public struct ContactUsView: View {
#ObservedObject private var contactUsVM: ContactUsViewModel
private var successAlert: Alert {
Alert(
title: Text("Email Sent"),
message: Text("Thanks for taking the time to reach out to us. We appreciate it!"),
dismissButton: .default(Text("OK")) {
self.dismissSelf()
}
)
}
public var body: some View {
Form {
Text("Hello World")
}
.alert(isPresented: self.$contactUsVM.contactAttemptSucceeded) {
self.successAlert
}
}
public init() {
self.contactUsVM = ContactUsViewModel()
}
private func dismissSelf() {
print("Dismissing!")
}
}
class ContactUsViewModel: ObservableObject {
#Published var contactAttemptSucceeded: Bool = true
}
You'll see this:
This seems to be fixed on macOS Big Sur. Unfortunately, for those folks, who need to support macOS Catalina(me included), the only workaround is to create alert using UIAlertController.
The way I did that, is dispatching notification to SceneDelegate instance, and presenting UIAlertController on UIHostingController:
NotificationCenter.default.addObserver(forName: .showMailUnavailableAlert, object: nil, queue: nil) { [weak self] _ in
let controller = UIAlertController(title: "Default email client is not configured.", preferredStyle: .alert)
controller.addAction(.init(title: "Ok", style: .cancel, handler: nil))
self?.window?.rootViewController?.present(controller, animated: true, completion: nil)
}
extension NSNotification.Name {
static let showMailUnavailableAlert = NSNotification.Name("Email not configured.")
}
I don't know how to fix the duplicate buttons but to get the alert to dismiss you might need to add this line under the ObservedObject line:
#Environment(\.presentationMode) var presentationMode
and then add this:
presentationMode.wrappedValue.dismiss()
to your dismissSelf() func.
This is what I gleaned from a Hacking Swift video by Paul Hudson.
i need to display the same alert with different titles in each time
so i want to create a function that takes a string and changes the title of the alert and then display this alert
my question is : where should i create this function ? and how call it from another viewControllers ?
You can extend UIAlertController by implementing your own custom class, then you present this controller where you want it to be presented just changing title. Below is the sample code, it works. Hope you will understand the concept.
import UIKit
extension UIAlertController {
class func alertControllerWithTitle(title: String, message: String) -> UIAlertController {
let controller = UIAlertController(title: title, message: message, preferredStyle: .Alert)
controller.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
return controller
}
}
class ViewController: UIViewController {
let someProperty = 0
var alertTitle = ""
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(animated: Bool) {
someMethod()
}
func someMethod() {
switch someProperty {
case 0:
alertTitle = "someProperty is 0"
case 1:
alertTitle = "someProperty is 1"
default:
alertTitle = "someProperty is default"
}
//present alert
let controller = UIAlertController.alertControllerWithTitle(alertTitle, message: "some message")
presentViewController(controller, animated: true, completion: nil)
}
}
The function can be created as a stand-alone function (i.e. not part of any struct or class). It can exist anywhere in the same module as the classes that call it such as the view controllers. Personally, I would place it in a file on its own (called <yourFunctionName>.swift) close to the view controllers, or in a utility file containing similar functions.
Because the function is defined in the same module as the view controllers, it can be called directly by them.
Create a common class , place this function in that class, it should return a configured UIAlertController , create a protocol for that function in that common class with the alert function having Title as parameter.Conform this protocol to the class you want to display the Alert in, call it by its delegate and present it in a self.presentViewController(.....
The simple way to display a UIAlertView, in swift is:
let alert = UIAlertView()
alert.title = "Alert!"
alert.message = "A wise message"
alert.addButtonWithTitle("Ok, thank you")
alert.show()
But this is now depreciated in iOS 9 and recommends using UIAlertController:
let myAlert: UIAlertController = UIAlertController(title: "Alert!", message: "Oh! Fancy", preferredStyle: .Alert)
myAlert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
self.presentViewController(myAlert, animated: true, completion: nil)
Which is great, but I'm using SpriteKit and inside a GameScene, which gives an error of Value of type 'GameScene' has no member 'presentViewController'...
Do I need to switch back to my ViewController to present this or is there a way to call it from a GameScene.
I found THIS answer, but it's Objective-C.
There are many ways to handle this situation, I do not recommend Jozemite Apps answer, because this will cause problems on apps with more than 1 view controller.(you want to present the alert on the current view controller, not the root)
My preferred way of doing it is through delegation.
What needs to be done is create a protocol to handle messaging:
import Foundation
protocol ViewControllerDelegate
{
func sendMessage(message:String);
}
In your view controller:
class ViewController : UIViewController, ViewControllerDelegate
{
...
func sendMessage(message:String)
{
//do alert view code here
}
//in the view controllers view did load event
func viewDidLoad()
{
var view = self.view as! GameSceneView
view.delegate = self
}
In your view code:
var delegate : ViewControllerDelegate
Finally in game scene where you want to present:
self.view.delegate?.sendMessage(message)
This way allows limited access to the VC, and can be modified with more options when needed.
Another way is to set up a notification system, and use NSNotificationCenter to pass a message from the scene to the current VC and have it send a message;
in ViewController
func viewDidLoad()
{
NSNotificationCenter.defaultCenter().addObserver(self,selector:"AlertMessage:",name:"AlertMessage",object:nil);
}
func AlertMessage(notification:NSNotification)
{
if(let userInfo = notification.userInfo)
{
let message = userInfo["message"]
....//do alert view call here
}
}
In Game scene code:
...at the spot you want to send a message
let userInfo = ["message":message];
NSNotificationCenter.defaultCenter.postNotificationNamed("AlertMessage",object:nil,userInfo:userInfo)
Another approach is to save the view controller pointer to game scene view:
//in Game Scene View code
var viewController : UIViewController;
//in the view controllers view did load event
func viewDidLoad()
{
var view = self.view as! GameSceneView
view.viewController = self
}
//finally in game scene where you want to present
let myAlert: UIAlertController = UIAlertController(title: "Alert!", message: "Oh! Fancy", preferredStyle: .Alert)
myAlert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
self.view.viewController.presentViewController(myAlert, animated: true, completion: nil)
Yet another way is to make your view controller global.
In view controller code:
private var _instance : UIViewController
class ViewController : UIViewController
{
class var instance
{
get
{
return _instance;
}
}
func viewDidLoad()
{
_instance = self;
}
}
Then just call
ViewController.instance!.
whenever you need access to your view controller.
Each of these methods have there strengths and weaknesses, so choose whatever way works best for you.
Try using this. I work in SpriteKit and I used this code for my in app purchase messages in my game, Chomp'd.
self.view?.window?.rootViewController?.presentViewController(myAlert, animated: true, completion: nil)
The answer from #Knight0fDragon is nice, but I think it's a little long.
I will just put here another solution using Podfile of Cocoapoad for new comers (others guys having the same problem).
first you need to install cocoapod and initiate it for your project (very easy to do; check some YouTube video or this link.
in your obtained Podfile, copy, and paste this: pod 'SIAlertView'. It is "A UIAlertView replacement with block syntax and fancy transition styles". (More details here. Please give credit to the libraries Authors you're using.
Finally, in your GameScene.swift file add the following after the import or after the closing bracket of the class GameScene
private extension GameScene {
func showPauseAlert() {
let alertView = SIAlertView(title: "Edess!!", andMessage: "Congratulations! test testing bla bla bla")
alertView.addButtonWithTitle("OK", type: .Default) { (alertView) -> Void in
print("ok was pushed")
}
alertView.show()
}
}
You can add many button with title if you want, and do whatever action you want. Here I just print "Ok was pushed".
The above cited links plus this one helped me to understand and work with this UIAlertView easily in my multiple-level game, to display alert on the "Pause" button of the game.