Home > OS >  Swift binding to a computed property
Swift binding to a computed property

Time:02-13

Have the following situation. I have a view model that is an observable object with a computed property of type Bool. I want to be able to enable/disable a navigation link based on the computed property, but I need a binding to do so. Here a simplified example:

struct Project {
    var name: String
    var duration: Int
}

class MyViewModel: Observable Object {
    @Published var project: Project

    var isProjectValid: Bool {
        return project.name != "" && project.duration > 0
    }
}

struct MyView: View {
    @EnvironmentObject var myVM: MyViewModel

    var body: some View {
        ...
        NavigationLink("Click Link", isActive: ?????, destination: NextView())
        ...
    }
}

Since isActive expects a binding I am not able to access computed property such as myVM.isProjectValid. Tried also with the computed property in project class, still same problem.

Also considered creating custom binding related to the computed property, but not quite sure if/how to go about it.

First question I have posted, so if I am missing some details please be kind :)

CodePudding user response:

Make it a @Published property and update it when project is changed

class MyViewModel: ObservableObject {
    @Published var project: Project {
        didSet {
            isProjectValid = project.name != "" && project.duration > 0
        }
    }

    @Published var isProjectValid: Bool

     //...
}

CodePudding user response:

@Binding is by definition a two-way connection

https://developer.apple.com/documentation/swiftui/binding

When the NavigationLink is dismissed the Binding will automatically try to make that variable false.

That being said you can easily make it a Binding by adding a {get set} the code below will work in your project.

var isProjectValid: Bool {
    get{
        project.name != "" && project.duration > 0
    }
    set{
        //Something has to happen here to make the `get` condition `false`
        if !newValue{
            project.duration = -1
        }
    }
}

But the issue is with the current setup of setting the duration to -1 that is likely not something you want to have done.

So you need to provide another variable that can hold the Binding that decides if the NavigationLink isActive or not, if you don't your variable will be out of sync.

Here is full working code I had to make some tweaks to get it to compile and run.

import SwiftUI

struct Project {
    var name: String
    var duration: Int
}

class MyViewModel: ObservableObject {
    @Published var project: Project = Project(name: "name", duration: -1)

    var isProjectValid: Bool {
        get{
            project.name != "" && project.duration > 0
        }
        set{
            //Something has to happen here to make the `get` condition `false`
            if !newValue{
                project.duration = -1
            }
        }
    }
}

struct ComputedBindingView: View {
    @StateObject var myVM: MyViewModel = .init()

    var body: some View {
        NavigationView{
        VStack{
            Button("toggle", action: {
                if myVM.isProjectValid{
                    myVM.project.duration = -1
                }else{
                    myVM.project.duration = 1
                }
                
            })
        NavigationLink("Click Link", isActive: $myVM.isProjectValid, destination: {Text("NextView()")})
        }
        }
    }
}
struct ComputedBindingView_Previews: PreviewProvider {
    static var previews: some View {
        ComputedBindingView()
    }
}

CodePudding user response:

The use of the computed property suggests some type of design where the user is not supposed to trigger the NavigationLink directly. But instead the NavigationLink is expected to be triggered programatically as a side-effect of some other mechanism elsewhere in the code. Such as might be done at the completion of a form or similar process by the user.

Not 100% if this is what's being aimed for, but if it is, then one option would be to pass a constant Binding to the NavigationLink, e.g.

NavigationLink("Click Link", isActive: .constant(myVM.isProjectValid), destination: NextView())`
  • Related