SwiftUI: Catalyst Alert Showing Duplicate Buttons and Not Triggering Action - ios

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.

Related

How do I display a SwiftUI alert from outside of the ContentView?

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
}
}
}

SwiftUI - Create popup that displays an updating progress message when executing long code

I am creating an app that has some code that takes a bit to execute. I want it to hang the app as I don't want the user to make any changes when it's executing. It's a Multiplatform app so when executing in macOS it automatically changes the mouse to a rolling ball image so that works-ish. In iOS there's no feedback at all. I want to create a popup (thinking alert but not too fussy) that displays an updating message showing the user what's happening and making it obvious they have to wait.
Right now my View calls a class that executes the code so I wanted to somehow pass a variable that gets updated in the class but is visible in the View in real-time. Ideally I would want to be able to use this and call different methods each time from other Views but still use a popup with messages updating the user while the code executes.
To simplify this I created a mini project but I can't get it to work on either the macOS OR iOS as the View (app) isn't updated until after the code finishes executing (also have print statements to know what's happening). I've been trying #StateObject, #Published, ObservableObject, etc to no avail.
Code: ContentView.swift
import SwiftUI
import CoreData
struct ContentView: View {
#StateObject var progress = myProgress()
#State var showProgress:Bool = false
var body: some View {
NavigationView {
VStack {
Button(action: {
print("Pressed Button 1")
showProgress = true
}, label: {
Text("Execute Option")
})
.sheet(isPresented: $showProgress, content: {
Text(progress.message)
.onAppear() {
Task {
let longMethod = longMethodsCalled(currProgress: progress)
print("About to call method - \(progress.message)")
let error = longMethod.exampleOne(title: "My Passed In Title")
// Here I can use the returned value if it's an object or now if it passed if it's String?
print("Error: \(error ?? "No error")")
print("after printing error - \(progress.message)")
}
// If this is commented out it just shows the last message after code finishes
showProgress = false
}
})
}
}
}
}
Other file: longMethodsCalled.swift
import Foundation
import SwiftUI
class myProgress: ObservableObject {
#Published var message = "progressing..."
func changeMessage(newMessage:String) {
message = newMessage
print("Message changing. Now: \(message)")
self.objectWillChange.send()
}
}
class longMethodsCalled {
#State var progress: myProgress
init(currProgress:myProgress) {
progress = currProgress
}
public func exampleOne(title:String) -> String? {
print("in example one - \(title)")
progress.changeMessage(newMessage: "Starting example one")
sleep(1)
print("after first sleep")
progress.changeMessage(newMessage: "Middle of example one")
sleep(1)
progress.changeMessage(newMessage: "About to return - example one")
return "result of example one"
}
}
I guess I'm wondering if this is even possible? And if so how can I go about it. I can't tell if I'm close or completely out to lunch. I would REALLY love a way to update my users when my code executes.
Thanks for any and all help.
Here is an example using binding to do all view update in another struct. It is using async and await. For the sleep(), it use Task.sleep which does not lock queues.
struct LongMethodCallMessage: View {
#State var showProgress:Bool = false
#State var progressViewMessage: String = "will do something"
var body: some View {
NavigationView {
VStack {
Button(action: {
print("Pressed Button 1")
progressViewMessage = "Pressed Button 1"
showProgress = true
}, label: {
// text will be return value
// so one can see that it ran
Text(progressViewMessage)
})
.sheet(isPresented: $showProgress, content: {
// create the vue that will display the progress
TheLongTaskView(progressViewMessage: $progressViewMessage, showProgress: $showProgress)
})
}
}
}
}
struct TheLongTaskView: View, LongMethodsCalledMessage {
#Binding var progressViewMessage: String
#Binding var showProgress: Bool
var body: some View {
Text(progressViewMessage)
.onAppear() {
// create the task setting this as delegate
// to receive message update
Task {
let longMethod = LongMethodsCalled(delegate: self)
print("About to call method - \(progressViewMessage)")
let error = await longMethod.exampleOne(title: "My Passed In Title")
// Here I can use the returned value if it's an object or now if it passed if it's String?
print("Error: \(error ?? "No error")")
print("after printing error - \(progressViewMessage)")
// save the error message and close view
progressViewMessage = error!
showProgress = false
}
}
}
// updating the text
func changeMessage(newMessage:String) {
print("changeMessage: \(newMessage)")
progressViewMessage = newMessage
}
}
// a protocol to update the text in the long running task
protocol LongMethodsCalledMessage {
func changeMessage(newMessage:String)
}
class LongMethodsCalled {
var delegate: LongMethodsCalledMessage
init(delegate: LongMethodsCalledMessage) {
self.delegate = delegate
}
// make the method async
public func exampleOne(title:String) async -> String? {
print("in example one - \(title)")
self.delegate.changeMessage(newMessage: "Starting example one")
// this wait enable the text to update
// the sleep() may lock and prevent main queue to run
try! await Task.sleep(nanoseconds: 2_000_000_000)
print("after first sleep")
self.delegate.changeMessage(newMessage: "Middle of example one")
try! await Task.sleep(nanoseconds: 2_000_000_000)
print("after second sleep")
self.delegate.changeMessage(newMessage: "About to return - example one")
return "result of example one"
}
}

SwiftUI: Send email using MFMailComposeViewController

I am currently trying to implement a "send email" button in my SwiftUI app, using SwiftUI lifecycle and targeting iOS 14.
I know there are quite some solutions presented online - here on stack overflow and elsewhere. However, I have-not been able to make anything work so far in simulator/on device.
My current solution looks like this (based on this question on stackoverflow:
import SwiftUI
import MessageUI
import Foundation
struct ContentView: View {
class MailComposeViewController: UIViewController, MFMailComposeViewControllerDelegate {
static let shared = MailComposeViewController()
func sendEmail() {
if MFMailComposeViewController.canSendMail() {
let mail = MFMailComposeViewController()
mail.mailComposeDelegate = self
mail.setToRecipients(["test#test.com"])
UIApplication.shared.windows.last?.rootViewController?.present(mail, animated: true, completion: nil)
} else {
// Alert
}
}
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true, completion: nil)
}
}
var body: some View {
Button(action: {
MailComposeViewController.shared.sendEmail()
}, label: {
Text("Send")
})
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The simulator does show the button and doesn't give me any errors. However, upon clicking the button, nothing happens at all - same thing when testing on device.
Any idea what might be wrong here?
Thanks!
Building up on the code snippet shared in my original question:
Based on the answer from #Arjun this is my current workaround to account for the edge case that someone might have deleted the Apple Mail app and is using another email app:
Button(action: {
if MailComposeViewController.shared.canSendMail() {
MailComposeViewController.shared.sendEmail()
} else {
openURL(URL(string: "mailto:someone#example.com?subject=This%20is%20the%20subject")!)
}
}, label: {
Text("Send")
})
It opens the in-app sheet as long as the user has set up apple mail and otherwise switches to any other email app using a mailto: link.
Your code works fine, the problem is that the iOS Simulator does not have the Mail app, thus MFMailComposeViewController.canSendMail() returns false. Try it on a physical device, it works. The reason you didn't see any errors is because of this block of code:
if MFMailComposeViewController.canSendMail() {
let mail = MFMailComposeViewController()
mail.mailComposeDelegate = self
mail.setToRecipients(["test#test.com"])
UIApplication.shared.windows.last?.rootViewController?.present(mail, animated: true, completion: nil)
} else {
// Alert
}
Inside the else block, instead of temporarily commenting // Alert, you should print something instead while debugging, it'll make your life a lot easier.

How to create an extension on a view to show alerts on long press gesture - SwiftUI

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")

How to present an Alert in SwiftUI with no buttons

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)
}

Resources