Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to bind presentation of SwiftUI.Alert when triggered outside of the View?

Most examples of showing Alert refer to some kind of @State being used as a binding that controls the presented/hidden state of the alert view.

As an example showingAlert (source):

struct ContentView : View {
    @State var showingAlert = false
    
    var body: some View {
        Button(action: {
            self.showingAlert = true
        }) {
            Text("Show Alert")
        }
        .alert(isPresented: $showingAlert) {
            Alert(
                title: Text("Important message"),
                message: Text("Wear sunscreen"),
                dismissButton: .default(Text("Got it!"))
            )
        }
    }
}

It's a good solution when the alert is triggered from the UI layer - as in the example:

Button(action: {
    self.showingAlert = true
}

But what if we want to trigger it from the controller/viewmodel layer with a specific message? As an example, we make a network call - the URLSession’s Publisher can send Data or an Error that we want to push to the user as a message in the Alert.

@State is designed to be managed from the view's body, so it seems that we should rather use an @ObjectBinding in this case. It seems that we also need some message, so we can reference it in the body:

Alert(
    title: Text("Important message"),
    message: Text(objectBinding.message)
)

The showingAlert would be a bit redundant here as we may define message as String? and create a binding for presentation:

Binding<Bool>(
    getValue: { objectBinding.message != nil },
    setValue: { if !$0 { objectBinding.message = nil } }
)

It's a doable approach and it works, but two things are making me a bit anxious:

  1. The fact that message is managed by two abstractions
  2. The information and management of the presented/hidden state of the alert leaked into controller/viewmodel/object binding. It'd be nice to keep the presented/hidden state privately in the view.
  3. The fact that message is kept in the controller/viewmodel/object binding until it gets kind of "consumed" by the view (the binding).

Can it be done better?

like image 517
Maciek Czarnik Avatar asked Nov 17 '22 06:11

Maciek Czarnik


1 Answers

If you specifically want to use Combine and it's publisher mechanism, you might be able to use onReceive(). Every generic SwiftUI View has onReceive() on it as a generic function that accepts a publisher, and when instantiated will subscribe to the publisher. It acts very similarly to the single-closure version of the Combine subscriber sink if you're familiar with that.

The specifics of the publisher expect a publisher Failure type of Never for this to work, so if you're working from Error's within some pipeline, you'll need to convert them to some other kind of object (perhaps an Enum with associated String values) and externalize them so they can be displayed with SwiftUI.

How you end up exposing the publisher to the SwiftUI view might be awkward as well - I've been doing it by adding a publisher onto a reference object with other @Published properties.

You can see a rough example of how this works (although not your specific use case) in Using Combine at https://heckj.github.io/swiftui-notes/#pattern-observableobject

like image 107
heckj Avatar answered Dec 10 '22 01:12

heckj