4

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.

Zouhair Sassi
  • 1,403
  • 1
  • 13
  • 30
FPL
  • 456
  • 4
  • 21
  • 1
    Can't you add the `round > 10 ?...` code to directly after the if/else? `showingAlert = round > 10 ? true : false`. By the way, your if and else is doing the same thing – Joakim Danielson Oct 27 '19 at 12:03

2 Answers2

5

You can simply use Binding.constant(_:). To convert computed property to binding property.


@State var round = 0
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: .constant(showingAlert)) {
                Alert(title: Text("title"), message: Text("message"), dismissButton: .default(Text("Continue"))
                )
} 
kumar shivang
  • 174
  • 2
  • 9
  • If I use this like `.alert(isPresented: .constant(showingAlert))`, the Alert can not be dismissed – below Mar 15 '21 at 21:34
  • 1
    Yes and no, both. Yes because you can't toggle showingAlert variable directly since it is computed property, but if you want to toggle you can simply change the dependent state variable such that it toggles the computed value, which is round <= 10 in this case. If you want to avoid computed property altogether then you can simply make a bool variable which changes on { willSet { .. }} of dependent variable. – kumar shivang Mar 18 '21 at 08:07
3

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.

dumbledad
  • 16,305
  • 23
  • 120
  • 273
  • 1
    Why should a model object have a property to determine if an alert should be shown or not, that creates an unnecessary dependency between model and controller layer. If you want to use a model object for this then it should publish the `round` property. Or at least rename it to something that fits the logic, like `gameIsOver` – Joakim Danielson Oct 27 '19 at 14:34
  • 1
    @JoakimDanielson, I guess it's a matter of style. (I see your point though.) But instead of an alert, let's make it a full screen modal - and with background processes happening. Maybe the model needs to know what's going on with the views. I guess the question is what *drives* the UI - the user interaction and model? Or does the UI *drive* the model? –  Oct 27 '19 at 14:40
  • I don't think the model should drive anything or know what happens in the controller or view layer, it should just hold a state. Then it is up to the controller layer to react on changes in that state or query the model object because some internal request or a request from the view. Note that I don't see anything wrong about your answer, this is more of a btw comment. – Joakim Danielson Oct 27 '19 at 18:05