Search code examples
iosswiftswiftuicomputed-propertiesproperty-binding

SwiftUI show alert based on computed property


I'm trying to show an alert in Swift based on a computed property. Basically, whenever a user clicks on a button the value of "round" is updated. When more than 10 rounds have taken place, an alert shows.

To do this, I've created a boolean variable called "showingAlert". This has to be a @State var so that it gets set to false again when the user closes the alert.

However, the compiler tells me that a property wrapper like @State "cannot be applied to a computed property" :-(

This is the code that I've tried:


@State var round = 0
@State var showingAlert:Bool {round > 10 ? true : false}

func result(player: Int, app: Int) {
if player > app {
 round += 1
 }
else {
 round += 1
}
}

var body: some View {
        Button(action: {self.result(player: 1, app: 1)}) {
        Text("Button")
        }
           .alert(isPresented: $showingAlert) {
                Alert(title: Text("title"), message: Text("message"), dismissButton: .default(Text("Continue"))
                )
} 

Is there any way round this? I'd love to create an alert that shows without error messages.


Solution

  • I prefer putting logic in a model - separation of logic from the view - but here's something that works:

    @State var round = 0
    @State var showingAlert:Bool = false
    
    func result(player: Int, app: Int) {
        if player > app {
            round += 1
        } else {
            round += 1
        }
        if round > 10 {
            showingAlert.toggle()
        }
    }
    

    Basically, move your check into your function. Notes:

    • I'm assuming this is test logic... if not, you have a typo in your if/else because they both do the same thing.
    • Only set showingAlert to true - let SwiftUI set it to false when the alert is dismissed.
    • The real reason to separate this logic from the view is you can make things easy to reset round. Here's the code that does it:
    import SwiftUI
    import Combine
    
    class Model : ObservableObject {
        var objectWillChange = PassthroughSubject<Void, Never>()
        @Published var showingAlert = false {
            willSet {
                objectWillChange.send()
                if newValue == false {
                    round = 0
                }
            }
        }
        var round = 0
        func result(player: Int, app: Int) {
            if player > app {
                round += 1
            } else {
                round += 1
            }
            if round > 10 {
                showingAlert.toggle()
            }
        }
    }
    
    struct ContentView: View {
        @EnvironmentObject var model: Model
    
        var body: some View {
            Button(action: {self.model.result(player: 1, app: 1)}) {
                Text("Button")
                }
            .alert(isPresented: self.$model.showingAlert) {
                    Alert(title: Text("title"), message: Text("message"), dismissButton: .default(Text("Continue")))
                }
        }
    }
    

    Note that there is only one variable (showingAlert) marked as @Published, that you can properly code against willSet, and all you need to change in ContentView is adding the EnvironmentObjectafter your add it to yourSceneDelegate` properly.

    The first set of code will show the alert after the 11th tap, and then every tap after that. The second set of code will show the alert after the 11th tap, then every 11 taps after that.