How to Consecutively Present Two Alert Views Using Swiftui

How to consecutively present two alert views using SwiftUI

It appears (tested with Xcode 11.2):

  1. While not documented, but it is not allowed to add more than one
    .alert modifier in one view builder sequence - works only latest
  2. It is not allowed to add .alert modifier to EmptyView, it does not work
    at all

I've found alternate solution to proposed by @Rohit. In some situations, many alerts, this might result in simpler code.

struct TestTwoAlerts: View {
@State var alertIsVisible = false
@State var bonusAlertIsVisible = false

var score = 100
var title = "First alert"

var body: some View {
VStack {
Button(action: {
self.alertIsVisible = true
}) {
Text("Hit Me!")
}
.alert(isPresented: $alertIsVisible) {
Alert(title: Text("\(title)"), message: Text("\n"), dismissButton:.default(Text("Next Round"), action: {
if self.score == 100 {
DispatchQueue.main.async { // !! This part important !!
self.bonusAlertIsVisible = true
}
}
}))
}
Text("")
.alert(isPresented: $bonusAlertIsVisible) {
Alert(title: Text("Bonus"), message: Text("You've earned 100 points bonus!!"), dismissButton: .default(Text("Close")))
}
}
}
}

How can I have two alerts on one view in SwiftUI?

The second call to .alert(isPresented) is overriding the first. What you really want is one Binding<Bool> to denote whether the alert is presented, and some setting for which alert should be returned from the closure following .alert(isPresented). You could use a Bool for this, but I went ahead and did it with an enum, as that scales to more than two alerts.

enum ActiveAlert {
case first, second
}

struct ToggleView: View {
@State private var showAlert = false
@State private var activeAlert: ActiveAlert = .first

var body: some View {

Button(action: {
if Bool.random() {
self.activeAlert = .first
} else {
self.activeAlert = .second
}
self.showAlert = true
}) {
Text("Show random alert")
}
.alert(isPresented: $showAlert) {
switch activeAlert {
case .first:
return Alert(title: Text("First Alert"), message: Text("This is the first alert"))
case .second:
return Alert(title: Text("Second Alert"), message: Text("This is the second alert"))
}
}
}
}

How to show two alerts in swift ui one followed by the other

You cannot make two .alert calls directly behind each other without overwriting one of them. The key is to get all the logic inside a single .alert call.

import SwiftUI

enum ActiveAlert {
case first, second, third
}

struct ContentView: View {
@State private var showingAlert = false
@State private var activeAlert: ActiveAlert = .first

var body: some View {
Button(action: {
self.showAlert(.first)
}, label: {
Text("button")
.alert(isPresented: $showingAlert) {
switch activeAlert {
case .first:
return Alert(title: Text("First Alert"), dismissButton: .default(Text("Next"), action: {
self.showAlert(.second)
}))
case .second:
return Alert(title: Text("Second Alert"), dismissButton: .default(Text("Next"), action: {
self.showAlert(.third)
}))
case .third:
return Alert(title: Text("Third Alert"), dismissButton: .default(Text("Ok"), action: {
//...
}))
}
}
})
}

func showAlert(_ active: ActiveAlert) -> Void {
DispatchQueue.global().async {
self.activeAlert = active
self.showingAlert = true
}
}
}

Multiple Alerts in one view can not be called SwiftUI

So i'm still not 100% sure why this was unable to post an alert this way? But I was able to fix this issue simply by placing the alert in the main view.

I changed the @State variable isResetting to a binding bool and paced a new state variable in the main class. I then just copied over the .alert and this seems to solve the issue.

The struct now looks like this

private struct resetButton: View {
@Binding var isResetting: Bool

var body: some View {
Button("Reset"){
self.isResetting = true
}

}
}

and the alert is the same but is now in the class calling resetButton().

Edit:

So it appears that SwiftUI won't let you call multiple alerts from the same view, however, if you want multiple alerts in the same view then there is a way around this.

You can call an alert from anywhere inside of a view so the following solution works.

private struct exampleAlert: View {
@State var alert1 = false
@State var alert2 = false

var body: some View {
Vstack{
Button("alert 1"){
self.alert1 = true
}.alert(isPresented: $alert1) {
Alert(title: Text("Important message"), message: Text("This alert can show"), dismissButton: .default(Text("Got it!")))
}
Button("alert 2"){
self.alert2 = true
}.alert(isPresented: $alert2) {
Alert(title: Text("Important message"), message: Text("This alert can also show"), dismissButton: .default(Text("Got it!")))
}
}
}
}

The catch is here that if either of these alerts were to be placed on the Vstack, one will not function. If they are placed on separate views though then both can be called as expected.

Perhaps this is something that will be rectified in a future update? In the meantime though, here is a solution for working around this problem.

Multiple lined UIAlertController message

It will work with \n as edited.

let alert = UIAlertController(title: "Title",
message: "message: \n message",
preferredStyle: UIAlertControllerStyle.alert)

let cancelAction = UIAlertAction(title: "OK",
style: .cancel, handler: nil)

alert.addAction(cancelAction)
self.present(alert, animated: true, completion: nil)

Swift UI - Using UIVIewControllerRepresentable to update logs

You haven't provided a Minimal Reproducible Example but here is a simplified version of what seems to be what you are trying to do.

First, add a LogManager that can be created by ANY class or struct

struct LogManager{
var name: String
///Simplified post that takes in the String and uses the name as the source
func postMessage(message: String){
postMessage(message: .init(timestamp: Date(), message: message, source: name))
}
//MARK: Notification
///Sends a Notification with the provided message
func postMessage(message: Message){
NotificationCenter.default.post(name: .logManagerMessage, object: message)
}
///Adds an observer to the manager's notification
func observeMessageNotification(observer: Any, selector: Selector){
NotificationCenter.default.addObserver(observer, selector: selector, name: .logManagerMessage, object: nil)
}
}

Put at class or struct level the declaration for the manager

private let log = LogManager(name: "YourClassStructName")

Then call

log.postMessage(message: "your message here")

When you want to log a message.

In the ViewModel you would

  1. subscribe to the notifications
  2. maintain the array

Like below

class AppLogSampleViewModel: ObservableObject{
static let shared: AppLogSampleViewModel = .init()
private let manager = LogManager(name: "AppLogSampleViewModel")
@Published var messages: [Message] = []
private init(){
//Observe the manager's notification
manager.observeMessageNotification(observer: self, selector: #selector(postMessage(notification:)))
}
///Puts the messages received into the array
@objc
func postMessage(notification: Notification){
if notification.object is Message{
messages.append(notification.object as! Message)
}else{
messages.append(.init(timestamp: Date(), message: "Notification received did not have message", source: "AppLogSampleViewModel :: \(#function)"))
}
}
}

If your View won't be at the highest level you need to call.

let startListening: AppLogSampleViewModel = .shared

In the ContentView or AppDelegate so the ViewModel starts listening as quickly as possible. You won't necessarily use it for anything but it needs to be called as soon as you want it to start logging messages.

Only the View that shows the messages uses the instance of the ViewModel.

struct AppLogSampleView: View {
@StateObject var vm: AppLogSampleViewModel = .shared
//Create this variable anywhere in your app
private let log = LogManager(name: "AppLogSampleView")
var body: some View {
List{
Button("post", action: {
//Post like this anywhere in your app
log.postMessage(message: "random from button")
})
DisclosureGroup("log messages"){
ForEach(vm.messages, content: { message in
VStack{
Text(message.timestamp.description)
Text(message.message)
Text(message.source)
}
})
}
}
}
}

Here is the rest of the code you need to get this sample working.

struct AppLogSampleView_Previews: PreviewProvider {
static var previews: some View {
AppLogSampleView()
}
}
extension Notification.Name {
static let logManagerMessage = Notification.Name("logManagerMessage")
}
struct Message: Identifiable{
let id: UUID = .init()
var timestamp: Date
var message: String
var source: String
}

Only the one View that shows the messages needs access to the ViewModel and only one ViewModel subscribes to the notifications.

All the other classes and structs will just create an instance of the manager and call the method to post the messages.

There is no sharing/passing between the classes and structs everybody gets their own instance.

Your manager can have as many or as little methods as you want, mine usually mimics the Logger from osLog with log, info, debug and error methods.

The specific methods call the osLog and 3rd party Analytics Services corresponding methods.

Also, the error method sends a notification that a ViewModel at the top level receives and shows an Alert.

To get all this detail working it takes a lot more code but you have the pattern with the code above.

In your code, in the updateUIViewController you break the single source if truth rule by copying the messages and putting them in another source of truth, right here.

fullLogMessage = logViewModel.getTheFullLogMessage()

This is also done without a check to make sure that you don't go into an infinite loop. Anytime there is code in an update method you should check that the work actually needs to be done. Such as comparing that the new location doesn't already match the old location.



Related Topics



Leave a reply



Submit