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

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

Related

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 modify state var from remote closure

I've got an application written in swift/swiftUI.
the logic part composed of events generator that enable the client to register a callback for each event before the UI part starts.
init.swift
-----------
func applicationDidFinishLaunching(_ aNotification: Notification) {
let event1Handler : eventHandler = {(data) in
ContentView().alert_handler.showAlert = true
}
let event2Handler : eventHandler = {(data) in
...
}
eventGenerator.sharedInstance().registerUIEvent1(event1Handler, event2: event2Handler)
window = NSWindow(...)
window.contentView = NSHostingView(rootView: ContentView())
...
}
in the UI part, there's an optional alert that is presented depent on the showAlert that can be set from the closure from the previous file...
class alertHandler: ObservableObject {
#Published var showAlert = false
}
struct ContentView: View {
#ObservedObject var alert_handler = alertHandler()
var body: some View {
GeometryReader { metrics in
.....
}.alert(isPresented: $alert_handler.showAlert, content: {
Alert(title: Text("Alert:"),
message: Text("press OK to execute default action..."),
dismissButton: Alert.Button.default(
Text("Press ok here"), action: { print("Hello world!") }
)
)
})
Unfortunately I couldn't see the alert appears when the first event was triggered. Perhaps anyone can tell me if I'd doing anything wrong ? or suggest an alternative approach to modify swiftUI struct variable (event_handler) from remote closure ?
I believe that my the problem may derived from ContentView module which is not a singleton, so when I set showAlert I'm doing so for another instance of ContentView. How can I fix my code to access showAlert that belongs to the currently running instance of ContentView ?
Your suspicion about ContentView not being a singleton instance is correct. You can solve this by owning your alertHandler in the parent (in this case, the app delegate) and passing that down to ContentView.
var handler = alertHandler()
func applicationDidFinishLaunching(_ aNotification: Notification) {
let event1Handler : eventHandler = { (data) in
self.handler.showAlert = true //<-- Here
}
let event2Handler : eventHandler = {(data) in
...
}
eventGenerator.sharedInstance().registerUIEvent1(event1Handler, event2: event2Handler)
window = NSWindow(...)
window.contentView = NSHostingView(rootView: ContentView(alert_handler: handler))
...
}
struct ContentView: View {
#ObservedObject var alert_handler : alertHandler //<-- Here
That way, when you modify the showAlert property, the ContentView gets updated because it's the same instance of alertHandler.
Note: I'd consider adopting the Swift conventions of capitalizing type names and using camel case rather than snake case -- it'll make it easier for others to read your code.

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

SwiftUI: Catalyst Alert Showing Duplicate Buttons and Not Triggering Action

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.

SwiftUI : Dismiss modal from child view

I'm attempting to dismiss a modal after its intended action is completed, but I have no idea how this can be currently done in SwiftUI. This modal is triggered by a #State value change. Would it be possible to change this value by observing a notification of sorts?
Desired actions: Root -> Initial Modal -> Presents Children -> Dismiss modal from any child
Below is what I've tried
Error: Escaping closure captures mutating 'self' parameter
struct AContentView: View {
#State var pageSaveInProgress: Bool = false
init(pages: [Page] = []) {
// Observe change to notify of completed action
NotificationCenter.default.publisher(for: .didCompletePageSave).sink { (pageSaveInProgress) in
self.pageSaveInProgress = false
}
}
var body: some View {
VStack {
//ETC
.sheet(isPresented: $pageSaveInProgress) {
ModalWithChildren()
}
}
}
}
ModalWithChildren test action
Button(action: {
NotificationCenter.default.post(
name: .didCompletePageSave, object: nil)},
label: { Text("Close") })
You can receive messages through .onReceive(_:perform) which can be called on any view. It registers a sink and saves the cancellable inside the view which makes the subscriber live as long as the view itself does.
Through it you can initiate #State attribute changes since it starts from the view body. Otherwise you would have to use an ObservableObject to which change can be initiated from anywhere.
An example:
struct MyView : View {
#State private var currentStatusValue = "ok"
var body: some View {
Text("Current status: \(currentStatusValue)")
}
.onReceive(MyPublisher.currentStatusPublisher) { newStatus in
self.currentStatusValue = newStatus
}
}
A complete example
import SwiftUI
import Combine
extension Notification.Name {
static var didCompletePageSave: Notification.Name {
return Notification.Name("did complete page save")
}
}
struct OnReceiveView: View {
#State var pageSaveInProgress: Bool = true
var body: some View {
VStack {
Text("Usual")
.onReceive(NotificationCenter.default.publisher(for: .didCompletePageSave)) {_ in
self.pageSaveInProgress = false
}
.sheet(isPresented: $pageSaveInProgress) {
ModalWithChildren()
}
}
}
}
struct ModalWithChildren: View {
#State var presentChildModals: Bool = false
var body: some View {
Button(action: {
NotificationCenter.default.post(
name: .didCompletePageSave,
object: nil
)
}) { Text("Send message") }
}
}

Resources